This commit is contained in:
Dain Nilsson 2022-02-11 10:22:49 +01:00
commit 56bd68296a
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
25 changed files with 2483 additions and 35 deletions

2
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "yubikey-manager"]
path = yubikey-manager
path = ykman-rpc/yubikey-manager
url = https://github.com/Yubico/yubikey-manager

View File

@ -1,16 +1,13 @@
@echo off
REM Make sure the submodule is cloned, but if it already is, don't reset it.
( dir /b /a "yubikey-manager" | findstr . ) > nul || (
git submodule init
git submodule update
)
REM Make sure the submodule is cloned and up to date.
git submodule update --init
echo Building ykman CLI for Windows...
cd yubikey-manager
echo Building ykman-rpc for Windows...
cd ykman-rpc
poetry install
rmdir /s /q ..\build\windows\ykman
poetry run pyinstaller ykman.spec --distpath ..\build\windows
rmdir /s /q ..\build\windows\ykman-rpc
poetry run pyinstaller ykman-rpc.spec --distpath ..\build\windows
cd ..
echo All done, output in build/windows/

View File

@ -6,11 +6,8 @@
set -e
# Make sure the submodule is cloned, but if it already is, don't reset it.
if ! [ "$(ls yubikey-manager)" ]; then
git submodule init
git submodule update
fi
# Make sure the submodule is cloned and up to date.
git submodule update --init
case "$(uname)" in
Darwin*)
@ -21,22 +18,22 @@ case "$(uname)" in
OS="windows";;
esac
echo "Building ykman CLI for $OS..."
echo "Building ykman-rpc for $OS..."
OUTPUT="build/$OS"
cd yubikey-manager
cd ykman-rpc
poetry install
rm -rf ../$OUTPUT/ykman
poetry run pyinstaller ykman.spec --distpath ../$OUTPUT
rm -rf ../$OUTPUT/ykman-rpc
poetry run pyinstaller ykman-rpc.spec --distpath ../$OUTPUT
cd ..
# Fixup permissions (should probably be more strict)
find $OUTPUT/ykman -type f -exec chmod a-x {} +
chmod a+x $OUTPUT/ykman/ykman
find $OUTPUT/ykman-rpc -type f -exec chmod a-x {} +
chmod a+x $OUTPUT/ykman-rpc/ykman-rpc
# Adhoc sign executable (MacOS)
if [ "$OS" = "macos" ]; then
codesign -f --timestamp --entitlements macos/ykman.entitlements --sign - $OUTPUT/ykman/ykman
codesign -f --timestamp --entitlements macos/ykman.entitlements --sign - $OUTPUT/ykman-rpc/ykman-rpc
fi
echo "All done, output in $OUTPUT/"

View File

@ -28,7 +28,7 @@ Future<List<Override>> initializeAndGetOverrides() async {
// Either use the _YKMAN_EXE environment variable, or look relative to executable.
var exe = Platform.environment['_YKMAN_PATH'];
if (exe?.isEmpty ?? true) {
var relativePath = 'ykman/ykman';
var relativePath = 'ykman-rpc/ykman-rpc';
if (Platform.isMacOS) {
relativePath = '../Resources/' + relativePath;
} else if (Platform.isWindows) {
@ -41,7 +41,7 @@ Future<List<Override>> initializeAndGetOverrides() async {
log.info('Starting subprocess: $exe');
var rpc = await RpcSession.launch(exe!);
log.info('ykman process started', exe);
log.info('ykman-rpc process started', exe);
rpc.setLogLevel(Logger.root.level);
return [

View File

@ -78,8 +78,7 @@ class RpcSession {
}
static Future<RpcSession> launch(String executable) async {
var process =
await Process.start(executable, [], environment: {'_YKMAN_RPC': '1'});
var process = await Process.start(executable, []);
return RpcSession(process);
}

View File

@ -115,5 +115,5 @@ if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
COMPONENT Runtime)
endif()
# Copy the ykman CLI
install(DIRECTORY "../build/linux/ykman" DESTINATION "${BUILD_BUNDLE_DIR}" USE_SOURCE_PERMISSIONS)
# Copy the ykman RPC
install(DIRECTORY "../build/linux/ykman-rpc" DESTINATION "${BUILD_BUNDLE_DIR}" USE_SOURCE_PERMISSIONS)

View File

@ -26,7 +26,7 @@
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
A549BDAB2747CBBE0016F37D /* ykman in Resources */ = {isa = PBXBuildFile; fileRef = A549BDAA2747CBBE0016F37D /* ykman */; };
A549BDAB2747CBBE0016F37D /* ykman-rpc in Resources */ = {isa = PBXBuildFile; fileRef = A549BDAA2747CBBE0016F37D /* ykman-rpc */; };
CCE73883AA6E76B42D34D392 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E5437883A25FD13EEA6A730E /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
@ -71,7 +71,7 @@
6EAF9B998D311C2D6DD1409C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
A549BDAA2747CBBE0016F37D /* ykman */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ykman; path = ../build/macos/ykman; sourceTree = "<group>"; };
A549BDAA2747CBBE0016F37D /* ykman-rpc */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "ykman-rpc"; path = "../build/macos/ykman-rpc"; sourceTree = "<group>"; };
E5437883A25FD13EEA6A730E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
F18D61C5361D1EF615E824EE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
FFD2BDD751CD366AEDC4D417 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
@ -122,7 +122,7 @@
33CC11242044D66E0003C045 /* Resources */ = {
isa = PBXGroup;
children = (
A549BDAA2747CBBE0016F37D /* ykman */,
A549BDAA2747CBBE0016F37D /* ykman-rpc */,
33CC10F22044A3C60003C045 /* Assets.xcassets */,
33CC10F42044A3C60003C045 /* MainMenu.xib */,
33CC10F72044A3C60003C045 /* Info.plist */,
@ -248,7 +248,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A549BDAB2747CBBE0016F37D /* ykman in Resources */,
A549BDAB2747CBBE0016F37D /* ykman-rpc in Resources */,
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
);

View File

@ -17,5 +17,5 @@ target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
add_dependencies(${BINARY_NAME} flutter_assemble)
# This can probably be done in a cleaner way.
file(COPY "../../build/windows/ykman" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/Release")
file(COPY "../../build/windows/ykman" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/Debug")
file(COPY "../../build/windows/ykman-rpc" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/Release")
file(COPY "../../build/windows/ykman-rpc" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/Debug")

3
ykman-rpc/.flake8 Normal file
View File

@ -0,0 +1,3 @@
[flake8]
max-line-length = 88
ignore = E203, W503

6
ykman-rpc/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
*.pyc
*.egg-info
*.egg/
.eggs/
build/
dist/

610
ykman-rpc/poetry.lock generated Normal file
View File

@ -0,0 +1,610 @@
[[package]]
name = "altgraph"
version = "0.17.2"
description = "Python graph (network) package"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "atomicwrites"
version = "1.4.0"
description = "Atomic file writes."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "attrs"
version = "21.4.0"
description = "Classes Without Boilerplate"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
[[package]]
name = "cffi"
version = "1.15.0"
description = "Foreign Function Interface for Python calling C code."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
pycparser = "*"
[[package]]
name = "click"
version = "8.0.3"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
[[package]]
name = "colorama"
version = "0.4.4"
description = "Cross-platform colored terminal text."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "cryptography"
version = "36.0.1"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
cffi = ">=1.12"
[package.extras]
docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
sdist = ["setuptools_rust (>=0.11.4)"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
[[package]]
name = "dataclasses"
version = "0.8"
description = "A backport of the dataclasses module for Python 3.6"
category = "main"
optional = false
python-versions = ">=3.6, <3.7"
[[package]]
name = "fido2"
version = "0.9.3"
description = "Python based FIDO 2.0 library"
category = "main"
optional = false
python-versions = ">=2.7.6,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
[package.dependencies]
cryptography = ">=1.5"
six = "*"
[package.extras]
pcsc = ["pyscard"]
[[package]]
name = "future"
version = "0.18.2"
description = "Clean single-source support for Python 3 and 2"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "importlib-metadata"
version = "4.8.3"
description = "Read metadata from Python packages"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
zipp = ">=0.5"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
perf = ["ipython"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
[[package]]
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "jeepney"
version = "0.7.1"
description = "Low-level, pure Python DBus protocol wrapper."
category = "main"
optional = false
python-versions = ">=3.6"
[package.extras]
test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio", "async-timeout"]
trio = ["trio", "async-generator"]
[[package]]
name = "keyring"
version = "23.4.1"
description = "Store and access your passwords safely."
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
importlib-metadata = ">=3.6"
jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""}
pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""}
SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""}
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"]
[[package]]
name = "macholib"
version = "1.15.2"
description = "Mach-O header analysis and editing"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
altgraph = ">=0.15"
[[package]]
name = "packaging"
version = "21.3"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]]
name = "pefile"
version = "2021.9.3"
description = "Python PE parsing module"
category = "dev"
optional = false
python-versions = ">=3.6.0"
[package.dependencies]
future = "*"
[[package]]
name = "pluggy"
version = "1.0.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "py"
version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pycparser"
version = "2.21"
description = "C parser in Python"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pyinstaller"
version = "4.9"
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
category = "dev"
optional = false
python-versions = "<3.11,>=3.6"
[package.dependencies]
altgraph = "*"
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
pefile = {version = ">=2017.8.1", markers = "sys_platform == \"win32\""}
pyinstaller-hooks-contrib = ">=2020.6"
pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""}
[package.extras]
encryption = ["tinyaes (>=1.0.0)"]
hook_testing = ["pytest (>=2.7.3)", "execnet (>=1.5.0)", "psutil"]
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2022.0"
description = "Community maintained hooks for PyInstaller"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "pyparsing"
version = "3.0.7"
description = "Python parsing module"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "pyscard"
version = "2.0.2"
description = "Smartcard module for Python."
category = "main"
optional = false
python-versions = "*"
[package.extras]
Gui = ["wxpython"]
Pyro = ["pyro"]
[[package]]
name = "pytest"
version = "7.0.0"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
py = ">=1.8.2"
tomli = ">=1.0.0"
[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
[[package]]
name = "pywin32"
version = "303"
description = "Python for Window Extensions"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "pywin32-ctypes"
version = "0.2.0"
description = ""
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "secretstorage"
version = "3.3.1"
description = "Python bindings to FreeDesktop.org Secret Service API"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
cryptography = ">=2.0"
jeepney = ">=0.6"
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "tomli"
version = "1.2.3"
description = "A lil' TOML parser"
category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "typing-extensions"
version = "4.0.1"
description = "Backported and Experimental Type Hints for Python 3.6+"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "yubikey-manager"
version = "4.1.0-dev0"
description = "Tool for managing your YubiKey configuration."
category = "main"
optional = false
python-versions = "^3.6"
develop = false
[package.dependencies]
click = "^6.0 || ^7.0 || ^8.0"
cryptography = ">=2.1, <39"
dataclasses = {version = "^0.8", markers = "python_version < \"3.7\""}
fido2 = ">=0.9, <1.0"
keyring = "<23.5"
pyscard = "^1.9 || ^2.0"
pywin32 = {version = ">=223", markers = "sys_platform == \"win32\""}
[package.source]
type = "directory"
url = "yubikey-manager"
[[package]]
name = "zipp"
version = "3.6.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "main"
optional = false
python-versions = ">=3.6"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
[metadata]
lock-version = "1.1"
python-versions = "^3.6"
content-hash = "3ad372e77a86f3c7a25683cbeea3f2f98aeac8dbb279b7610429a62d23ba9325"
[metadata.files]
altgraph = [
{file = "altgraph-0.17.2-py2.py3-none-any.whl", hash = "sha256:743628f2ac6a7c26f5d9223c91ed8ecbba535f506f4b6f558885a8a56a105857"},
{file = "altgraph-0.17.2.tar.gz", hash = "sha256:ebf2269361b47d97b3b88e696439f6e4cbc607c17c51feb1754f90fb79839158"},
]
atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
]
attrs = [
{file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
]
cffi = [
{file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"},
{file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"},
{file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"},
{file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"},
{file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"},
{file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"},
{file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"},
{file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"},
{file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"},
{file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"},
{file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"},
{file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"},
{file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"},
{file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"},
{file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"},
{file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"},
{file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"},
{file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"},
{file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"},
{file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"},
{file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"},
{file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"},
{file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"},
{file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"},
{file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"},
]
click = [
{file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"},
{file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
cryptography = [
{file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b"},
{file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31"},
{file = "cryptography-36.0.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a"},
{file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173"},
{file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12"},
{file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3"},
{file = "cryptography-36.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2"},
{file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f"},
{file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3"},
{file = "cryptography-36.0.1-cp36-abi3-win32.whl", hash = "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca"},
{file = "cryptography-36.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf"},
{file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9"},
{file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1"},
{file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94"},
{file = "cryptography-36.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac"},
{file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee"},
{file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46"},
{file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903"},
{file = "cryptography-36.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316"},
{file = "cryptography-36.0.1.tar.gz", hash = "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638"},
]
dataclasses = [
{file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"},
{file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"},
]
fido2 = [
{file = "fido2-0.9.3.tar.gz", hash = "sha256:b45e89a6109cfcb7f1bb513776aa2d6408e95c4822f83a253918b944083466ec"},
]
future = [
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
]
importlib-metadata = [
{file = "importlib_metadata-4.8.3-py3-none-any.whl", hash = "sha256:65a9576a5b2d58ca44d133c42a241905cc45e34d2c06fd5ba2bafa221e5d7b5e"},
{file = "importlib_metadata-4.8.3.tar.gz", hash = "sha256:766abffff765960fcc18003801f7044eb6755ffae4521c8e8ce8e83b9c9b0668"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
jeepney = [
{file = "jeepney-0.7.1-py3-none-any.whl", hash = "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac"},
{file = "jeepney-0.7.1.tar.gz", hash = "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f"},
]
keyring = [
{file = "keyring-23.4.1-py3-none-any.whl", hash = "sha256:17e49fb0d6883c2b4445359434dba95aad84aabb29bbff044ad0ed7100232eca"},
{file = "keyring-23.4.1.tar.gz", hash = "sha256:89cbd74d4683ed164c8082fb38619341097741323b3786905c6dac04d6915a55"},
]
macholib = [
{file = "macholib-1.15.2-py2.py3-none-any.whl", hash = "sha256:885613dd02d3e26dbd2b541eb4cc4ce611b841f827c0958ab98656e478b9e6f6"},
{file = "macholib-1.15.2.tar.gz", hash = "sha256:1542c41da3600509f91c165cb897e7e54c0e74008bd8da5da7ebbee519d593d2"},
]
packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
pefile = [
{file = "pefile-2021.9.3.tar.gz", hash = "sha256:344a49e40a94e10849f0fe34dddc80f773a12b40675bf2f7be4b8be578bdd94a"},
]
pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
py = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
pycparser = [
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
]
pyinstaller = [
{file = "pyinstaller-4.9-py3-none-macosx_10_13_universal2.whl", hash = "sha256:e2f165cea4470ce8a8349112cd78f48a61413805adc17792a91997a11cfe1d80"},
{file = "pyinstaller-4.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:24035eb9fffa2e3e288b4c1c9710043819efc7203cae5c8c573bec16f4a8e98f"},
{file = "pyinstaller-4.9-py3-none-manylinux2014_i686.whl", hash = "sha256:a0b988cfc197d40e3d773b3aa1c7d3e918fc0933b4c15ec3fc5d156f222d82cb"},
{file = "pyinstaller-4.9-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:62c97cbbdbee30974d607eb1de9afb081eb3adba787c203b00438e21027b829b"},
{file = "pyinstaller-4.9-py3-none-manylinux2014_s390x.whl", hash = "sha256:7f46ab11ec986e4c525b93251063144e12d432a132dbc0070e3030e34c76537a"},
{file = "pyinstaller-4.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:b5f1a94150315ea75bf3501be6c8476d65a7209580bb662da06dbdbc4454f375"},
{file = "pyinstaller-4.9-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:ebeb87cdbadb2b4e8f991ffd9945ebd4fb3a7303180e63682c3e1ce01b3fdd22"},
{file = "pyinstaller-4.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:59372b950d176fdc5ecea29719a8ab3f194b73a15b7f9875ac2a1de9a3daf5ed"},
{file = "pyinstaller-4.9-py3-none-win32.whl", hash = "sha256:ec3ca331d565ffca1b6470c5aaf798885a03708c3d0b15c1b19009126f84c1d4"},
{file = "pyinstaller-4.9-py3-none-win_amd64.whl", hash = "sha256:bec57b3b2b6178907255557ec0fc4b5ce5a0474013414cdadea853205c74ed26"},
{file = "pyinstaller-4.9.tar.gz", hash = "sha256:75a180a658871bc41f9cf94b6f90ffa54e98f5d6a7cdb02d7530f0360afe24f9"},
]
pyinstaller-hooks-contrib = [
{file = "pyinstaller-hooks-contrib-2022.0.tar.gz", hash = "sha256:61b667f51b2525377fae30793f38fd9752a08032c72b209effabf707c840cc38"},
{file = "pyinstaller_hooks_contrib-2022.0-py2.py3-none-any.whl", hash = "sha256:29f0bd8fbb2ff6f2df60a0c147e5b5ad65ae5c1a982d90641a5f712de03fa161"},
]
pyparsing = [
{file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"},
{file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"},
]
pyscard = [
{file = "pyscard-2.0.2-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:eeeca096bc89feec311b44b9f2e9050157c6e87d9addada975e9ce479b3e39b7"},
{file = "pyscard-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:07e4a091f1c75c3d426ae23c935cfa9e8bdd6860c99aeccdcc0eb28a895ec0b5"},
{file = "pyscard-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:cfb2d8f05f0850637e25c21975b6a1ed01582308937a8c9e02e38d43f6495422"},
{file = "pyscard-2.0.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:68773baf5aaf88a2a9862aa724e5d2fd43a75374e45cbb77beeb313d47fe3f31"},
{file = "pyscard-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:3147be0097f88e18dca286e4a7aa58ac016c0a7a2f1b49f91ea42d7d46ac5fe8"},
{file = "pyscard-2.0.2.tar.gz", hash = "sha256:05de0579c42b4eb433903aa2fb327d4821ebac262434b6584da18ed72053fd9e"},
]
pytest = [
{file = "pytest-7.0.0-py3-none-any.whl", hash = "sha256:42901e6bd4bd4a0e533358a86e848427a49005a3256f657c5c8f8dd35ef137a9"},
{file = "pytest-7.0.0.tar.gz", hash = "sha256:dad48ffda394e5ad9aa3b7d7ddf339ed502e5e365b1350e0af65f4a602344b11"},
]
pywin32 = [
{file = "pywin32-303-cp310-cp310-win32.whl", hash = "sha256:6fed4af057039f309263fd3285d7b8042d41507343cd5fa781d98fcc5b90e8bb"},
{file = "pywin32-303-cp310-cp310-win_amd64.whl", hash = "sha256:51cb52c5ec6709f96c3f26e7795b0bf169ee0d8395b2c1d7eb2c029a5008ed51"},
{file = "pywin32-303-cp311-cp311-win32.whl", hash = "sha256:d9b5d87ca944eb3aa4cd45516203ead4b37ab06b8b777c54aedc35975dec0dee"},
{file = "pywin32-303-cp311-cp311-win_amd64.whl", hash = "sha256:fcf44032f5b14fcda86028cdf49b6ebdaea091230eb0a757282aa656e4732439"},
{file = "pywin32-303-cp36-cp36m-win32.whl", hash = "sha256:aad484d52ec58008ca36bd4ad14a71d7dd0a99db1a4ca71072213f63bf49c7d9"},
{file = "pywin32-303-cp36-cp36m-win_amd64.whl", hash = "sha256:2a09632916b6bb231ba49983fe989f2f625cea237219530e81a69239cd0c4559"},
{file = "pywin32-303-cp37-cp37m-win32.whl", hash = "sha256:b1675d82bcf6dbc96363fca747bac8bff6f6e4a447a4287ac652aa4b9adc796e"},
{file = "pywin32-303-cp37-cp37m-win_amd64.whl", hash = "sha256:c268040769b48a13367221fced6d4232ed52f044ffafeda247bd9d2c6bdc29ca"},
{file = "pywin32-303-cp38-cp38-win32.whl", hash = "sha256:5f9ec054f5a46a0f4dfd72af2ce1372f3d5a6e4052af20b858aa7df2df7d355b"},
{file = "pywin32-303-cp38-cp38-win_amd64.whl", hash = "sha256:793bf74fce164bcffd9d57bb13c2c15d56e43c9542a7b9687b4fccf8f8a41aba"},
{file = "pywin32-303-cp39-cp39-win32.whl", hash = "sha256:7d3271c98434617a11921c5ccf74615794d97b079e22ed7773790822735cc352"},
{file = "pywin32-303-cp39-cp39-win_amd64.whl", hash = "sha256:79cbb862c11b9af19bcb682891c1b91942ec2ff7de8151e2aea2e175899cda34"},
]
pywin32-ctypes = [
{file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"},
{file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"},
]
secretstorage = [
{file = "SecretStorage-3.3.1-py3-none-any.whl", hash = "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f"},
{file = "SecretStorage-3.3.1.tar.gz", hash = "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
tomli = [
{file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"},
{file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"},
]
typing-extensions = [
{file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"},
{file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"},
]
yubikey-manager = []
zipp = [
{file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"},
{file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"},
]

20
ykman-rpc/pyproject.toml Normal file
View File

@ -0,0 +1,20 @@
[tool.poetry]
name = "ykman-rpc"
version = "0.1.0"
description = "Yubico Authenticator helper app"
authors = ["Dain Nilsson <dain@yubico.com>"]
[tool.poetry.dependencies]
python = "^3.6"
yubikey-manager = {path = "yubikey-manager"}
[tool.poetry.dev-dependencies]
pyinstaller = {version = "^4.9", python = "<3.11"}
pytest = "^7.0.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.pytest.ini_options]
testpaths = ["tests"]

231
ykman-rpc/rpc-shell.py Executable file
View File

@ -0,0 +1,231 @@
#!/usr/bin/env python3
import cmd
import json
import click
import subprocess # nosec
import sys
import logging
from typing import IO, cast
logger = logging.getLogger(__name__)
def red(value):
return f"\u001b[31;1m{value}\u001b[0m"
def green(value):
return f"\u001b[32;1m{value}\u001b[0m"
def yellow(value):
return f"\u001b[33;1m{value}\u001b[0m"
def cyan(value):
return f"\u001b[36;1m{value}\u001b[0m"
class RpcShell(cmd.Cmd):
def __init__(self, stdin, stdout):
super().__init__()
self._stdin = stdin
self._stdout = stdout
self._echo = False
self._path = []
self._node = None
self.do_cd(None)
def _send(self, data):
if self._echo:
print("SEND:", cyan(json.dumps(data)))
json.dump(data, self._stdin)
self._stdin.write("\n")
self._stdin.flush()
def _recv(self):
line = self._stdout.readline()
if self._echo:
print("RECV:", cyan(line))
try:
return json.loads(line)
except Exception:
print("failed to parse:", line)
raise
@property
def prompt(self):
return "/" + "/".join(self._path) + "> "
def resolve_path(self, line):
if line:
parts = line.split("/")
if parts[0]:
parts = self._path + parts
else:
parts.pop(0)
while ".." in parts:
pos = parts.index("..")
parts.pop(pos - 1)
parts.pop(pos - 1)
else:
parts = self._path + [""]
return parts
def completepath(self, text, nodes_only=False):
target = self.resolve_path(text)
cmd = target.pop() if target else ""
node = self.get_node(target)
if node:
names = [n + "/" for n in node.get("children", [])]
if not nodes_only:
actions = node.get("actions", [])
if "get" in actions:
actions.remove("get")
names += actions
res = [n for n in names if n.startswith(cmd)]
return res
return []
def completedefault(self, cmd, text, *args):
return self.completepath(text)
def completenames(self, cmd, text, *ignored):
return self.completepath(text)
def emptyline(self):
self.do_ls(None)
def get_node(self, target):
logger.debug("sending get: %r", target)
self._send({"kind": "command", "action": "get", "target": target})
result = self._recv()
logger.debug("got info: %r", result)
kind = result["kind"]
if kind == "success":
return result
elif kind == "error":
status = result["status"]
print(red(f"{status.upper()}: {result['body']}"))
else:
print(red(f"Invalid response: {result}"))
def do_echo(self, args):
self._echo = not self._echo
print("ECHO is", "on" if self._echo else "off")
def do_quit(self, args):
return True
def do_cd(self, args):
if args:
target = self.resolve_path(args)
if target and not target[-1]:
target.pop()
else:
target = []
logger.debug("Get info for %r", target)
if self.get_node(target):
self._path = target
logger.debug("set path %r", target)
def complete_cd(self, cmd, text, *args):
return self.completepath(text[3:], True)
def do_ls(self, args):
self._send({"kind": "command", "action": "get", "target": self._path})
result = self._recv()
kind = result["kind"]
if kind == "success":
self._node = result["body"]
data = self._node.get("data", None)
if data:
for k, v in data.items():
print(yellow(f"{k}: {v}"))
for c, c_data in self._node.get("children", {}).items():
print(green(f"{c}/"))
if c_data:
for k, v in c_data.items():
print(yellow(f" {k}: {v}"))
for name in self._node.get("actions", []):
if name != "get": # Don't show get, always available
print(cyan(f"{name}"))
elif kind == "error":
status = result["status"]
print(red(f"{status.upper()}: {result['body']}"))
else:
print(red(f"Invalid response: {result}"))
def default(self, line):
parts = line.strip().split(maxsplit=1)
if len(parts) == 2:
try:
args = json.loads(parts[1])
if not isinstance(args, dict):
logger.error("Argument must be a JSON Object")
return
except json.JSONDecodeError as e:
logger.error("Error decoding JSON.", exc_info=e)
return
else:
args = {}
target = self.resolve_path(parts[0])
action = target.pop()
self._send(
{
"kind": "command",
"action": action or "get",
"target": target,
"body": args,
}
)
while True:
result = self._recv()
kind = result["kind"]
if kind == "signal":
print(cyan(f"{result['status']}: {result.get('body', None)}"))
else:
break
if kind == "success":
body = result.get("body", None)
if body:
print(yellow(json.dumps(body)))
elif kind == "error":
print(red(f"{result['status']}: {result['message']}"))
body = result.get("body", None)
if result:
print(red(json.dumps(body)))
else:
print(red(f"Invalid response: {result}"))
def do_EOF(self, args):
return True
@click.command()
@click.argument("executable", nargs=-1)
def shell(executable):
"""A basic shell for interacting with the ykman rpc."""
rpc = subprocess.Popen( # nosec
executable or [sys.executable, "ykman-rpc.py"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
encoding="utf8",
)
click.echo("Shell starting...")
shell = RpcShell(rpc.stdin, cast(IO[str], rpc.stdout))
shell.cmdloop()
click.echo("Stopping...")
rpc.communicate()
if __name__ == "__main__":
shell()

161
ykman-rpc/rpc/__init__.py Normal file
View File

@ -0,0 +1,161 @@
# Copyright (c) 2021 Yubico AB
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from .base import RpcException, encode_bytes
from .device import RootNode
from queue import Queue
from threading import Thread, Event
from typing import Callable, Dict, List
import json
import logging
logger = logging.getLogger(__name__)
class _JsonLoggingFormatter(logging.Formatter):
def format(self, record):
data = {
"time": record.created,
"name": record.name,
"level": record.levelname,
"message": record.getMessage(),
}
if record.exc_info:
if not record.exc_text:
record.exc_text = self.formatException(record.exc_info)
data["exc_text"] = record.exc_text
return json.dumps(data)
def _init_logging():
logging.disable(logging.NOTSET)
logging.basicConfig()
logging.root.handlers[0].setFormatter(_JsonLoggingFormatter())
def _handle_incoming(event, recv, error, cmd_queue):
while True:
request = recv()
if not request:
break
try:
kind = request["kind"]
if kind == "signal":
# Cancel signals are handled here, the rest forwarded
if request["status"] == "cancel":
event.set()
else:
# Ignore other signals
logger.error("Unhandled signal: %r", request)
elif kind == "command":
cmd_queue.join() # Wait for existing command to complete
event.clear() # Reset event for next command
cmd_queue.put(request)
else:
error("invalid-command", "Unsupported request type")
except KeyError as e:
error("invalid-command", str(e))
except RpcException as e:
error(e.status, e.message, e.body)
except Exception as e:
error("exception", f"{e!r}")
event.set()
cmd_queue.put(None)
def process(
send: Callable[[Dict], None],
recv: Callable[[], Dict],
handler: Callable[[str, List, Dict, Event, Callable[[str], None]], Dict],
) -> None:
def error(status: str, message: str, body: Dict = {}):
send(dict(kind="error", status=status, message=message, body=body))
def signal(status: str, body: Dict = {}):
send(dict(kind="signal", status=status, body=body))
def success(body: Dict):
send(dict(kind="success", body=body))
event = Event()
cmd_queue: Queue = Queue(1)
read_thread = Thread(target=_handle_incoming, args=(event, recv, error, cmd_queue))
read_thread.start()
while True:
request = cmd_queue.get()
if request is None:
break
try:
success(
handler(
request["action"],
request.get("target", []),
request.get("body", {}),
event,
signal,
)
)
except RpcException as e:
error(e.status, e.message, e.body)
except Exception as e:
error("exception", f"{e!r}")
cmd_queue.task_done()
read_thread.join()
def run_rpc(
send: Callable[[Dict], None],
recv: Callable[[], Dict],
) -> None:
process(send, recv, RootNode())
def run_rpc_pipes(stdout, stdin):
_init_logging()
def _json_encode(value):
if isinstance(value, bytes):
return encode_bytes(value)
raise TypeError(type(value))
def send(data):
json.dump(data, stdout, default=_json_encode)
stdout.write("\n")
stdout.flush()
def recv():
line = stdin.readline()
if line:
return json.loads(line.strip())
return None
run_rpc(send, recv)

204
ykman-rpc/rpc/base.py Normal file
View File

@ -0,0 +1,204 @@
# Copyright (c) 2021 Yubico AB
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from functools import partial
import logging
logger = logging.getLogger(__name__)
def encode_bytes(value: bytes) -> str:
return value.hex()
decode_bytes = bytes.fromhex
class RpcException(Exception):
"""An exception that is returned as the result of an RPC command.i
Types:
invalid-command
state-reset
exception
"""
def __init__(self, status, message, body=None):
self.status = status
self.message = message
self.body = body or {}
super().__init__(message)
class InvalidParametersException(RpcException):
def __init__(self, message):
super().__init__("invalid-command", f"Invalid parameters: {message}")
class NoSuchActionException(RpcException):
def __init__(self, name):
super().__init__("invalid-command", f"No such action: {name}")
class NoSuchNodeException(RpcException):
def __init__(self, name):
super().__init__("invalid-command", f"No such node: {name}")
class StateResetException(RpcException):
def __init__(self, message, path):
super().__init__(
"state-reset", message or "State reset in node", dict(path=path)
)
class TimeoutException(RpcException):
def __init__(self):
super().__init__("timeout", "Command timed out waiting for user action")
class ChildResetException(Exception):
def __init__(self, message):
self.message = message
super().__init__()
MARKER_ACTION = "_rpc_action_marker"
MARKER_CHILD = "_rpc_child_marker"
def action(func=None, *, closes_child=True, condition=None):
if not func:
return partial(action, closes_child=closes_child, condition=condition)
setattr(func, MARKER_ACTION, dict(closes_child=closes_child, condition=condition))
return func
def child(func=None, *, condition=None):
if not func:
return partial(child, condition=condition)
setattr(func, MARKER_CHILD, dict(condition=condition))
return func
class RpcNode:
def __init__(self):
self._child = None
self._child_name = None
def __call__(self, action, target, params, event, signal, traversed=None):
traversed = traversed or []
try:
if target:
traversed += [target[0]]
return self.get_child(target[0])(
action, target[1:], params, event, signal, traversed
)
if action in self.list_actions():
return self.get_action(action)(params, event, signal)
if action in self.list_children():
traversed += [action]
return self.get_child(action)(
"get", [], params, event, signal, traversed
)
except ChildResetException as e:
self._close_child()
raise StateResetException(e.message, traversed)
except ValueError as e:
raise InvalidParametersException(e)
raise NoSuchActionException(action)
def close(self):
if self._child:
self._close_child()
def get_data(self):
return dict()
def _list_marked(self, marker):
children = {}
for name in dir(self):
options = getattr(getattr(self, name), marker, None)
if options is not None:
condition = options["condition"]
if condition is None or condition(self):
children[name] = options
return children
def list_actions(self):
return list(self._list_marked(MARKER_ACTION))
def get_action(self, name):
action = getattr(self, name, None)
options = getattr(action, MARKER_ACTION, None)
if options is not None:
if options["closes_child"] and self._child:
self._close_child()
return action
raise NoSuchActionException(name)
def list_children(self):
return {name: {} for name in self._list_marked(MARKER_CHILD).keys()}
def create_child(self, name):
child = getattr(self, name, None)
if hasattr(child, MARKER_CHILD):
return child()
raise NoSuchNodeException(name)
def _close_child(self):
if self._child:
logger.debug("close existing child: %s", self._child_name)
try:
self._child.close()
except Exception as e:
logger.error("Error closing child", exc_info=e)
self._child = None
self._child_name = None
def get_child(self, name):
if self._child and self._child_name != name:
self._close_child()
if not self._child:
self._child = self.create_child(name)
self._child_name = name
logger.debug("created child: %s", name)
return self._child
@action
def get(self, params, event, signal):
return dict(
data=self.get_data(),
actions=self.list_actions(),
children=self.list_children(),
)

348
ykman-rpc/rpc/device.py Normal file
View File

@ -0,0 +1,348 @@
# Copyright (c) 2021 Yubico AB
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from .base import RpcNode, child, action, NoSuchNodeException, ChildResetException
from .oath import OathNode
from .fido import Ctap2Node
from .yubiotp import YubiOtpNode
from .management import ManagementNode
from ykman import __version__ as ykman_version
from ykman.base import PID
from ykman.device import (
scan_devices,
list_all_devices,
get_name,
read_info,
)
from ykman.diagnostics import get_diagnostics
from yubikit.core import TRANSPORT
from yubikit.core.smartcard import SmartCardConnection, ApduError, SW
from yubikit.core.otp import OtpConnection
from yubikit.core.fido import FidoConnection
from yubikit.management import CAPABILITY
from ykman.pcsc import list_devices, YK_READER_NAME
from smartcard.Exceptions import SmartcardException
from dataclasses import asdict
from typing import Mapping, Tuple
import os
import logging
logger = logging.getLogger(__name__)
class RootNode(RpcNode):
def __init__(self):
super().__init__()
self._devices = DevicesNode()
self._readers = ReadersNode()
def __call__(self, *args):
result = super().__call__(*args)
if result is None:
result = {}
return result
def get_child(self, name):
self._child = self.create_child(name)
self._child_name = name
return self._child
def get_data(self):
return dict(version=ykman_version)
@child
def usb(self):
return self._devices
@child
def nfc(self):
return self._readers
@action
def diagnose(self, *ignored):
return dict(diagnostics=get_diagnostics())
@action(closes_child=False)
def logging(self, params, event, signal):
level = params["level"].upper()
log_level_value = getattr(logging, level)
logging.getLogger().setLevel(log_level_value)
logger.info(f"Log level set to: {level}")
return dict()
class ReadersNode(RpcNode):
def __init__(self):
super().__init__()
self._state = set()
self._readers = {}
self._reader_mapping = {}
@action(closes_child=False)
def scan(self, *ignored):
return self.list_children()
def list_children(self):
devices = [
d for d in list_devices("") if YK_READER_NAME not in d.reader.name.lower()
]
state = {d.reader.name for d in devices}
if self._state != state:
self._readers = {}
self._reader_mapping = {}
for device in devices:
dev_id = os.urandom(4).hex()
self._reader_mapping[dev_id] = device
self._readers[dev_id] = dict(name=device.reader.name)
self._state = state
return self._readers
def create_child(self, name):
return ReaderDeviceNode(self._reader_mapping[name], None)
class _ScanDevices:
def __init__(self):
self._state: Tuple[Mapping[PID, int], int] = ({}, 0)
self._caching = False
def __call__(self):
if not self._caching or not self._state[1]:
self._state = scan_devices()
return self._state
def __enter__(self):
self._caching = True
self._state = ({}, 0)
def __exit__(self, exc_type, exc, exc_tb):
self._caching = False
class DevicesNode(RpcNode):
def __init__(self):
super().__init__()
self._get_state = _ScanDevices()
self._list_state = 0
self._devices = {}
self._device_mapping = {}
def __call__(self, *args, **kwargs):
with self._get_state:
return super().__call__(*args, **kwargs)
@action(closes_child=False)
def scan(self, *ignored):
return self.get_data()
def get_data(self):
state = self._get_state()
return dict(state=state[1], pids=state[0])
def list_children(self):
state = self._get_state()
if state[1] != self._list_state:
self._devices = {}
self._device_mapping = {}
for dev, info in list_all_devices():
dev_id = str(info.serial) if info.serial else os.urandom(4).hex()
while dev_id in self._device_mapping:
dev_id = os.urandom(4).hex()
self._device_mapping[dev_id] = (dev, info)
name = get_name(info, dev.pid.get_type() if dev.pid else None)
self._devices[dev_id] = dict(pid=dev.pid, name=name, serial=info.serial)
if len(state[0]) == len(self._devices):
self._list_state = state[1]
else:
logger.warning("Not all devices identified")
self._list_state = 0
return self._devices
def create_child(self, name):
return UsbDeviceNode(*self._device_mapping[name])
class AbstractDeviceNode(RpcNode):
def __init__(self, device, info):
super().__init__()
self._device = device
self._info = info
def __call__(self, *args, **kwargs):
try:
return super().__call__(*args, **kwargs)
except (SmartcardException, OSError) as e:
logger.error("Device error", exc_info=e)
self._child = None
name = self._child_name
self._child_name = None
raise NoSuchNodeException(name)
def create_child(self, name):
try:
return super().create_child(name)
except (SmartcardException, OSError) as e:
logger.error(f"Unable to create child {name}", exc_info=e)
raise NoSuchNodeException(name)
def get_data(self):
for conn_type in (SmartCardConnection, OtpConnection, FidoConnection):
if self._device.supports_connection(conn_type):
with self._device.open_connection(conn_type) as conn:
pid = self._device.pid
self._info = read_info(pid, conn)
name = get_name(self._info, pid.get_type() if pid else None)
return dict(
pid=pid,
name=name,
transport=self._device.transport,
info=asdict(self._info),
)
raise ValueError("No supported connections")
class UsbDeviceNode(AbstractDeviceNode):
def __init__(self, device, info):
super().__init__(device, info)
def _supports_connection(self, conn_type):
return self._device.supports_connection(conn_type)
def _create_connection(self, conn_type):
connection = self._device.open_connection(conn_type)
return ConnectionNode(self._device.transport, connection, self._info)
@child(condition=lambda self: self._supports_connection(SmartCardConnection))
def ccid(self):
return self._create_connection(SmartCardConnection)
@child(condition=lambda self: self._supports_connection(OtpConnection))
def otp(self):
return self._create_connection(OtpConnection)
@child(condition=lambda self: self._supports_connection(FidoConnection))
def fido(self):
return self._create_connection(FidoConnection)
class ReaderDeviceNode(AbstractDeviceNode):
def get_data(self):
try:
return super().get_data() | dict(present=True)
except Exception:
return dict(present=False)
@child
def ccid(self):
connection = self._device.open_connection(SmartCardConnection)
info = read_info(None, connection)
return ConnectionNode(self._device.transport, connection, info)
@child
def fido(self):
with self._device.open_connection(SmartCardConnection) as conn:
info = read_info(None, conn)
connection = self._device.open_connection(FidoConnection)
return ConnectionNode(self._device.transport, connection, info)
class ConnectionNode(RpcNode):
def __init__(self, transport, connection, info):
super().__init__()
self._transport = transport
self._connection = connection
self._info = info or read_info(None, self._connection)
def __call__(self, *args, **kwargs):
try:
return super().__call__(*args, **kwargs)
except (SmartcardException, OSError) as e:
logger.error("Connection error", exc_info=e)
raise ChildResetException(f"{e}")
except ApduError as e:
if e.sw == SW.INVALID_INSTRUCTION:
raise ChildResetException(f"SW: {e.sw}")
raise e
@property
def capabilities(self):
return self._info.config.enabled_capabilities[self._transport]
def close(self):
super().close()
try:
self._connection.close()
except Exception as e:
logger.warning("Error closing connection", exc_info=e)
def get_data(self):
if (
isinstance(self._connection, SmartCardConnection)
or self._transport == TRANSPORT.USB
):
self._info = read_info(None, self._connection)
return dict(version=self._info.version, serial=self._info.serial)
@child(
condition=lambda self: self._transport == TRANSPORT.USB
or isinstance(self._connection, SmartCardConnection)
)
def management(self):
return ManagementNode(self._connection)
@child(
condition=lambda self: isinstance(self._connection, SmartCardConnection)
and CAPABILITY.OATH in self.capabilities
)
def oath(self):
return OathNode(self._connection)
@child(
condition=lambda self: isinstance(self._connection, FidoConnection)
and CAPABILITY.FIDO2 in self.capabilities
)
def ctap2(self):
return Ctap2Node(self._connection)
@child(
condition=lambda self: CAPABILITY.OTP in self.capabilities
and (
isinstance(self._connection, OtpConnection)
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)
)
)
)
)
def yubiotp(self):
return YubiOtpNode(self._connection)

247
ykman-rpc/rpc/fido.py Normal file
View File

@ -0,0 +1,247 @@
# Copyright (c) 2021 Yubico AB
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from .base import RpcNode, action, child
from fido2.ctap2 import (
Ctap2,
ClientPin,
CredentialManagement,
FPBioEnrollment,
CaptureError,
)
class Ctap2Node(RpcNode):
def __init__(self, connection):
super().__init__()
self.ctap = Ctap2(connection)
self._info = self.ctap.info
self.client_pin = ClientPin(self.ctap)
self._pin = None
def get_data(self):
self._info = self.ctap.get_info()
data = dict(info=self._info.data, locked=False)
if self._info.options.get("clientPin"):
data["locked"] = self._pin is None
pin_retries, power_cycle = self.client_pin.get_pin_retries()
data.update(
pin_retries=pin_retries,
power_cycle=power_cycle,
)
if self._info.options.get("bioEnroll"):
uv_retries = self.client_pin.get_uv_retries()
if isinstance(uv_retries, tuple):
uv_retries = uv_retries[0]
data.update(uv_retries=uv_retries)
return data
@action
def reset(self, params, event, signal):
self.ctap.reset(event)
self._pin = None
return dict()
@action(condition=lambda self: self._info.options["clientPin"])
def verify_pin(self, params, event, signal):
pin = params.pop("pin")
self.client_pin.get_pin_token(
pin, ClientPin.PERMISSION.GET_ASSERTION, "ykman.example.com"
)
self._pin = pin
@action
def set_pin(self, params, event, signal):
has_pin = self.ctap.get_info().options["clientPin"]
if has_pin:
self.client_pin.change_pin(
params.pop("pin"),
params.pop("new_pin"),
)
else:
self.client_pin.set_pin(
params.pop("new_pin"),
)
self._pin = None
@child(condition=lambda self: "bioEnroll" in self._info.options and self._pin)
def fingerprints(self):
token = self.client_pin.get_pin_token(
self._pin, ClientPin.PERMISSION.BIO_ENROLL
)
bio = FPBioEnrollment(self.ctap, self.client_pin.protocol, token)
return FingerprintsNode(bio)
# TODO: Use CredentialManagement.is_supported when released
@child(condition=lambda self: self._pin)
def credentials(self):
token = self.client_pin.get_pin_token(
self._pin, ClientPin.PERMISSION.CREDENTIAL_MGMT
)
creds = CredentialManagement(self.ctap, self.client_pin.protocol, token)
return CredentialsRpsNode(creds)
class CredentialsRpsNode(RpcNode):
def __init__(self, credman):
super().__init__()
self.credman = credman
self.refresh()
def refresh(self):
data = self.credman.get_metadata()
if data.get(CredentialManagement.RESULT.EXISTING_CRED_COUNT) == 0:
self._rps = {}
else:
self._rps = {
rp[CredentialManagement.RESULT.RP]["id"]: dict(
rp_id=rp[CredentialManagement.RESULT.RP]["id"],
rp_id_hash=rp[CredentialManagement.RESULT.RP_ID_HASH],
)
for rp in self.credman.enumerate_rps()
}
def list_children(self):
return self._rps
def create_child(self, name):
if name in self._rps:
return CredentialsRpNode(self.credman, self._rps[name], self.refresh)
return super().create_child(name)
class CredentialsRpNode(RpcNode):
def __init__(self, credman, rp_data, refresh):
super().__init__()
self.credman = credman
self.data = rp_data
self.refresh_rps = refresh
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"],
)
for cred in self.credman.enumerate_creds(self.data["rp_id_hash"])
}
def list_children(self):
return self._creds
def create_child(self, name):
if name in self._creds:
return CredentialNode(
self.credman,
self._creds[name],
self.refresh,
)
return super().create_child(name)
class CredentialNode(RpcNode):
def __init__(self, credman, credential_data, refresh):
super().__init__()
self.credman = credman
self.data = credential_data
self.refresh = refresh
def get_data(self):
return self.data
@action
def delete(self, params, event, signal):
self.credman.delete_cred(self.data["credential_id"])
self.refresh()
class FingerprintsNode(RpcNode):
def __init__(self, bio):
super().__init__()
self.bio = bio
self.refresh()
def refresh(self):
self._templates = self.bio.enumerate_enrollments()
def list_children(self):
return {
template_id.hex(): dict(name=name)
for template_id, name in self._templates.items()
}
def create_child(self, name):
template_id = bytes.fromhex(name)
if template_id in self._templates:
return FingerprintNode(
self.bio, template_id, self._templates[template_id], self.refresh
)
return super().create_child(name)
@action
def add(self, params, event, signal):
name = params.get("name", None)
enroller = self.bio.enroll()
template_id = None
while template_id is None:
try:
template_id = enroller.capture(event)
signal("capture", dict(remaining=enroller.remaining))
except CaptureError as e:
signal("capture-error", dict(code=e.code))
if name:
self.bio.set_name(template_id, name)
self._templates[template_id] = name
return dict(template_id=template_id, name=name)
class FingerprintNode(RpcNode):
def __init__(self, bio, template_id, name, refresh):
super().__init__()
self.bio = bio
self.refresh = refresh
self.template_id = template_id
self.name = name
def get_data(self):
return dict(template_id=self.template_id, name=self.name)
@action
def rename(self, params, event, signal):
name = params.pop("name")
self.bio.set_name(self.template_id, name)
self.name = name
self.refresh()
@action
def delete(self, params, event, signal):
self.bio.remove_enrollment(self.template_id)
self.refresh()

View File

@ -0,0 +1,73 @@
# Copyright (c) 2021 Yubico AB
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from .base import RpcNode, action
from yubikit.core import require_version, NotSupportedError
from yubikit.management import ManagementSession, DeviceConfig
from dataclasses import asdict
class ManagementNode(RpcNode):
def __init__(self, connection):
super().__init__()
self.session = ManagementSession(connection)
def get_data(self):
return asdict(self.session.read_device_info())
def list_actions(self):
actions = super().list_actions()
try:
require_version(self.session.version, (5, 0, 0))
actions.remove("set_mode")
except NotSupportedError:
actions.remove("configure")
return actions
@action
def configure(self, params, event, signal):
reboot = params.pop("reboot", False)
cur_lock_code = bytes.fromhex(params.pop("cur_lock_code", "")) or None
new_lock_code = bytes.fromhex(params.pop("new_lock_code", "")) or None
config = DeviceConfig(
params.pop("enabled_capabilities", {}),
params.pop("auto_eject_timeout", None),
params.pop("challenge_response_timeout", None),
params.pop("device_flags", None),
)
self.session.write_device_config(config, reboot, cur_lock_code, new_lock_code)
return dict()
@action
def set_mode(self, params, event, signal):
self.session.set_mode(
params.pop("mode"),
params.pop("challenge_response_timeout", 0),
params.pop("auto_eject_timeout", 0),
)
return dict()

300
ykman-rpc/rpc/oath.py Normal file
View File

@ -0,0 +1,300 @@
# Copyright (c) 2021 Yubico AB
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from .base import (
RpcNode,
action,
child,
ChildResetException,
TimeoutException,
RpcException,
encode_bytes,
decode_bytes,
)
from ykman.settings import AppData
from yubikit.core import require_version, NotSupportedError
from yubikit.core.smartcard import ApduError, SW
from yubikit.oath import OathSession, CredentialData, OATH_TYPE, HASH_ALGORITHM
from dataclasses import asdict
from time import time
import hmac
import os
import logging
logger = logging.getLogger(__name__)
class AuthRequiredException(RpcException):
def __init__(self):
super().__init__("auth-required", "Authentication is required")
def _get_keys():
return AppData("oath_keys")
class OathNode(RpcNode):
def __init__(self, connection):
super().__init__()
self.session = OathSession(connection)
if self.session.locked:
keys = _get_keys()
if keys.keyring_available and self.session.device_id in keys:
try:
key = bytes.fromhex(keys.get_secret(self.session.device_id))
self._do_validate(key)
except ApduError as e:
# Delete wrong key and fall through to prompt
if e.sw == SW.INCORRECT_PARAMETERS:
del keys[self.session.device_id]
keys.write()
except Exception as e:
# Other error, fall though to prompt
logger.warning("Error authenticating", exc_info=e)
def __call__(self, *args, **kwargs):
try:
return super().__call__(*args, **kwargs)
except ApduError as e:
if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED:
raise AuthRequiredException()
# TODO: This should probably be in a baseclass of all "AppNodes".
raise ChildResetException(f"SW: {e.sw:x}")
def get_data(self):
keys = _get_keys()
return dict(
version=self.session.version,
device_id=self.session.device_id,
has_key=self.session.has_key,
locked=self.session.locked,
remembered=self.session.device_id in keys,
)
@action
def derive(self, params, event, signal):
return dict(key=self.session.derive_key(params.pop("password")))
@action
def forget(self, params, event, signal):
keys = _get_keys()
del keys[self.session.device_id]
keys.write()
return dict()
def _remember_key(self, key):
keys = _get_keys()
if key is None:
if self.session.device_id in keys:
del keys[self.session.device_id]
keys.write()
else:
keys.put_secret(self.session.device_id, key.hex())
keys.write()
def _get_key(self, params):
has_key = "key" in params
has_pw = "password" in params
if has_key and has_pw:
raise ValueError("Only one of 'key' and 'password' can be provided.")
if has_pw:
return self.session.derive_key(params.pop("password"))
if has_key:
return decode_bytes(params.pop("key"))
raise ValueError("One of 'key' and 'password' must be provided.")
def _do_validate(self, key):
self.session.validate(key)
salt = os.urandom(32)
digest = hmac.new(salt, key, "sha256").digest()
self._key_verifier = (salt, digest)
@action
def validate(self, params, event, signal):
remember = params.pop("remember", False)
key = self._get_key(params)
if self.session.locked:
try:
self._do_validate(key)
if remember:
self._remember_key(key)
result = True
except ApduError as e:
if e.sw == SW.INCORRECT_PARAMETERS:
return dict(success=False)
raise e
elif hasattr(self, "_key_verifier"):
salt, digest = self._key_verifier
verify = hmac.new(salt, key, "sha256").digest()
result = hmac.compare_digest(digest, verify)
else:
result = False
return dict(success=result)
@action
def set_key(self, params, event, signal):
remember = params.pop("remember", False)
key = self._get_key(params)
self.session.set_key(key)
self._remember_key(key if remember else None)
return dict()
@action(condition=lambda self: self.session.has_key)
def unset_key(self, params, event, signal):
self.session.unset_key()
self._remember_key(None)
return dict()
@action
def reset(self, params, event, signal):
self.session.reset()
self._remember_key(None)
return dict()
@child(condition=lambda self: not self.session.locked)
def accounts(self):
return CredentialsNode(self.session)
class CredentialsNode(RpcNode):
def __init__(self, session):
super().__init__()
self.session = session
self.refresh()
def refresh(self):
# N.B. We use 'calculate_all' since it tells us if a TOTP credential
# requires touch or not.
self._creds = {c.id: c for c in self.session.calculate_all().keys()}
if self._child and self._child_name not in self._creds:
self._close_child()
def list_children(self):
return {encode_bytes(c_id): asdict(c) for c_id, c in self._creds.items()}
def create_child(self, name):
key = decode_bytes(name)
if key in self._creds:
return CredentialNode(self.session, self._creds[key], self.refresh)
return super().create_child(name)
@action
def calculate_all(self, params, event, signal):
timestamp = params.pop("timestamp", None)
result = self.session.calculate_all(timestamp)
return dict(
entries=[
dict(credential=asdict(cred), code=(asdict(code) if code else None))
for (cred, code) in result.items()
]
)
@action
def put(self, params, event, signal):
require_touch = params.pop("require_touch", False)
if "uri" in params:
data = CredentialData.parse_uri(params.pop("uri"))
if params:
raise ValueError("Unsupported parameters present")
else:
data = CredentialData(
params.pop("name"),
OATH_TYPE[params.pop("oath_type").upper()],
HASH_ALGORITHM[params.pop("hash", "sha1".upper())],
decode_bytes(params.pop("secret")),
**params,
)
if data.get_id() in self._creds:
raise ValueError("Credential already exists")
credential = self.session.put_credential(data, require_touch)
self._creds[credential.id] = credential
return asdict(credential)
class CredentialNode(RpcNode):
def __init__(self, session, credential, refresh):
super().__init__()
self.session = session
self.credential = credential
self.refresh = refresh
def _require_version(self, major, minor, micro):
try:
require_version(self.session.version, (major, minor, micro))
return True
except NotSupportedError:
return False
def get_info(self):
return asdict(self.credential)
@action
def code(self, params, event, signal):
timestamp = params.pop("timestamp", None)
try:
start = time()
code = self.session.calculate_code(self.credential, timestamp)
return asdict(code)
except ApduError as e:
if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED and time() - start > 5:
raise TimeoutException()
raise
@action
def calculate(self, params, event, signal):
challenge = decode_bytes(params.pop("challenge"))
try:
start = time()
response = self.session.calculate(self.credential.id, challenge)
return dict(response=response)
except ApduError as e:
if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED and time() - start > 5:
raise TimeoutException()
raise
@action
def delete(self, params, event, signal):
self.session.delete_credential(self.credential.id)
self.refresh()
self.credential = None
return dict()
@action(condition=lambda self: self._require_version(5, 3, 1))
def rename(self, params, event, signal):
name = params.pop("name")
issuer = params.pop("issuer", None)
try:
new_id = self.session.rename_credential(self.credential.id, name, issuer)
self.refresh()
return dict(credential_id=new_id)
except ApduError as e:
if e.sw == SW.INCORRECT_PARAMETERS:
raise ValueError("Issuer/name too long")
raise e

175
ykman-rpc/rpc/yubiotp.py Normal file
View File

@ -0,0 +1,175 @@
# Copyright (c) 2021 Yubico AB
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from .base import RpcNode, action, child
from yubikit.yubiotp import (
YubiOtpSession,
SLOT,
UpdateConfiguration,
HmacSha1SlotConfiguration,
HotpSlotConfiguration,
StaticPasswordSlotConfiguration,
YubiOtpSlotConfiguration,
StaticTicketSlotConfiguration,
)
class YubiOtpNode(RpcNode):
def __init__(self, connection):
super().__init__()
self.session = YubiOtpSession(connection)
def get_data(self):
state = self.session.get_config_state()
data = dict(
is_led_inverted=state.is_led_inverted(),
slot1_configured=state.is_configured(SLOT.ONE),
slot2_configured=state.is_configured(SLOT.TWO),
)
if self.session.version >= (3, 0, 0):
data.update(
slot1_touch_triggered=state.is_touch_triggered(SLOT.ONE),
slot2_touch_triggered=state.is_touch_triggered(SLOT.TWO),
)
return data
@action
def swap(self, params, event, signal):
self.session.swap_slots()
return dict()
@child
def one(self):
return SlotNode(self.session, SLOT.ONE)
@child
def two(self):
return SlotNode(self.session, SLOT.TWO)
_CONFIG_TYPES = dict(
hmac_sha1=HmacSha1SlotConfiguration,
hotp=HotpSlotConfiguration,
static_password=StaticPasswordSlotConfiguration,
yubiotp=YubiOtpSlotConfiguration,
static_ticket=StaticTicketSlotConfiguration,
)
class SlotNode(RpcNode):
def __init__(self, session, slot):
super().__init__()
self.session = session
self.slot = slot
self._state = self.session.get_config_state()
def get_data(self):
self._state = self.session.get_config_state()
data = dict(is_configured=self._state.is_configured(self.slot))
if self.session.version >= (3, 0, 0):
data.update(is_touch_triggered=self._state.is_touch_triggered(self.slot))
return data
@action(condition=lambda self: self._state.is_configured(self.slot))
def delete(self, params, event, signal):
self.session.delete_slot(self.slot, params.pop("cur_acc_code", None))
@action(
condition=lambda self: self._state.is_configured(self.slot)
and not self._state.is_touch_triggered(self.slot)
)
def calculate(self, params, event, signal):
challenge = bytes.fromhex(params.pop("challenge"))
response = self.session.calculate_hmac_sha1(self.slot, challenge, event)
return dict(response=response)
def _apply_config(self, config, params):
for option in (
"serial_api_visible",
"serial_usb_visible",
"allow_update",
"dormant",
"invert_led",
"protect_slot2",
"require_touch",
"lt64",
"append_cr",
"use_numeric",
"fast_trigger",
"digits8",
"imf",
"send_reference",
"short_ticket",
"manual_update",
):
if option in params:
getattr(config, option)(params.pop(option))
for option in ("tabs", "delay", "pacing", "strong_password"):
if option in params:
getattr(config, option)(*params.pop(option))
if "token_id" in params:
token_id, *args = params.pop("token_id")
config.token_id(bytes.fromhex(token_id), *args)
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()
@action(condition=lambda self: self._state.is_configured(self.slot))
def update(self, params, event, signal):
config = UpdateConfiguration()
self._apply_config(config, params)
self.session.update_configuration(
self.slot,
config,
params.pop("acc_code", None),
params.pop("cur_acc_code", None),
)
return dict()

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows Vista/Server 2008 -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
<!-- Windows 7/Server 2008 R2 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
<!-- Windows 8/Server 2012 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 8.1/Server 2012 R2 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

8
ykman-rpc/ykman-rpc.py Normal file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env python3
from rpc import run_rpc_pipes
import sys
if __name__ == "__main__":
run_rpc_pipes(sys.stdout, sys.stdin)

52
ykman-rpc/ykman-rpc.spec Executable file
View File

@ -0,0 +1,52 @@
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(
["ykman-rpc.py"],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name="ykman-rpc",
icon="NONE",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
manifest="ykman-rpc.exe.manifest",
version="yubikey-manager/resources/win/version_info.txt",
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name="ykman-rpc",
)

@ -0,0 +1 @@
Subproject commit ecd2500c8045c2deb89166cc58362e979639c3a9

@ -1 +0,0 @@
Subproject commit f90e4d6f59e8acc4399e9ff587f8c821456d069c