From fcab9b733174560ac36785eb788301c4b16d4937 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Thu, 10 Feb 2022 13:29:20 +0100 Subject: [PATCH 1/4] Start setting up ykman-rpc. --- .gitmodules | 2 +- ykman-rpc/.flake8 | 3 + ykman-rpc/.gitignore | 6 + ykman-rpc/poetry.lock | 610 +++++++++++++++++++++++++++++++ ykman-rpc/pyproject.toml | 20 + ykman-rpc/rpc-shell.py | 231 ++++++++++++ ykman-rpc/rpc/__init__.py | 161 ++++++++ ykman-rpc/rpc/base.py | 204 +++++++++++ ykman-rpc/rpc/device.py | 348 ++++++++++++++++++ ykman-rpc/rpc/fido.py | 247 +++++++++++++ ykman-rpc/rpc/management.py | 73 ++++ ykman-rpc/rpc/oath.py | 300 +++++++++++++++ ykman-rpc/rpc/yubiotp.py | 175 +++++++++ ykman-rpc/ykman-rpc.exe.manifest | 17 + ykman-rpc/ykman-rpc.py | 8 + ykman-rpc/ykman-rpc.spec | 52 +++ ykman-rpc/yubikey-manager | 1 + yubikey-manager | 1 - 18 files changed, 2457 insertions(+), 2 deletions(-) create mode 100644 ykman-rpc/.flake8 create mode 100644 ykman-rpc/.gitignore create mode 100644 ykman-rpc/poetry.lock create mode 100644 ykman-rpc/pyproject.toml create mode 100755 ykman-rpc/rpc-shell.py create mode 100644 ykman-rpc/rpc/__init__.py create mode 100644 ykman-rpc/rpc/base.py create mode 100644 ykman-rpc/rpc/device.py create mode 100644 ykman-rpc/rpc/fido.py create mode 100644 ykman-rpc/rpc/management.py create mode 100644 ykman-rpc/rpc/oath.py create mode 100644 ykman-rpc/rpc/yubiotp.py create mode 100644 ykman-rpc/ykman-rpc.exe.manifest create mode 100644 ykman-rpc/ykman-rpc.py create mode 100755 ykman-rpc/ykman-rpc.spec create mode 160000 ykman-rpc/yubikey-manager delete mode 160000 yubikey-manager diff --git a/.gitmodules b/.gitmodules index 6097639f..1374b5cb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "yubikey-manager"] - path = yubikey-manager + path = ykman-rpc/yubikey-manager url = https://github.com/Yubico/yubikey-manager diff --git a/ykman-rpc/.flake8 b/ykman-rpc/.flake8 new file mode 100644 index 00000000..abbd0dcd --- /dev/null +++ b/ykman-rpc/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 88 +ignore = E203, W503 diff --git a/ykman-rpc/.gitignore b/ykman-rpc/.gitignore new file mode 100644 index 00000000..b1c21d72 --- /dev/null +++ b/ykman-rpc/.gitignore @@ -0,0 +1,6 @@ +*.pyc +*.egg-info +*.egg/ +.eggs/ +build/ +dist/ diff --git a/ykman-rpc/poetry.lock b/ykman-rpc/poetry.lock new file mode 100644 index 00000000..e6b02adf --- /dev/null +++ b/ykman-rpc/poetry.lock @@ -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"}, +] diff --git a/ykman-rpc/pyproject.toml b/ykman-rpc/pyproject.toml new file mode 100644 index 00000000..c3065220 --- /dev/null +++ b/ykman-rpc/pyproject.toml @@ -0,0 +1,20 @@ +[tool.poetry] +name = "ykman-rpc" +version = "0.1.0" +description = "Yubico Authenticator helper app" +authors = ["Dain Nilsson "] + +[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"] diff --git a/ykman-rpc/rpc-shell.py b/ykman-rpc/rpc-shell.py new file mode 100755 index 00000000..9d97adcb --- /dev/null +++ b/ykman-rpc/rpc-shell.py @@ -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() diff --git a/ykman-rpc/rpc/__init__.py b/ykman-rpc/rpc/__init__.py new file mode 100644 index 00000000..2df6d2c2 --- /dev/null +++ b/ykman-rpc/rpc/__init__.py @@ -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) diff --git a/ykman-rpc/rpc/base.py b/ykman-rpc/rpc/base.py new file mode 100644 index 00000000..9d9b0326 --- /dev/null +++ b/ykman-rpc/rpc/base.py @@ -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(), + ) diff --git a/ykman-rpc/rpc/device.py b/ykman-rpc/rpc/device.py new file mode 100644 index 00000000..5b9cc93b --- /dev/null +++ b/ykman-rpc/rpc/device.py @@ -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) diff --git a/ykman-rpc/rpc/fido.py b/ykman-rpc/rpc/fido.py new file mode 100644 index 00000000..aaed4c6b --- /dev/null +++ b/ykman-rpc/rpc/fido.py @@ -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() diff --git a/ykman-rpc/rpc/management.py b/ykman-rpc/rpc/management.py new file mode 100644 index 00000000..83b2f4ff --- /dev/null +++ b/ykman-rpc/rpc/management.py @@ -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() diff --git a/ykman-rpc/rpc/oath.py b/ykman-rpc/rpc/oath.py new file mode 100644 index 00000000..b60cf170 --- /dev/null +++ b/ykman-rpc/rpc/oath.py @@ -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 diff --git a/ykman-rpc/rpc/yubiotp.py b/ykman-rpc/rpc/yubiotp.py new file mode 100644 index 00000000..948493f6 --- /dev/null +++ b/ykman-rpc/rpc/yubiotp.py @@ -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() diff --git a/ykman-rpc/ykman-rpc.exe.manifest b/ykman-rpc/ykman-rpc.exe.manifest new file mode 100644 index 00000000..9a388e31 --- /dev/null +++ b/ykman-rpc/ykman-rpc.exe.manifest @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/ykman-rpc/ykman-rpc.py b/ykman-rpc/ykman-rpc.py new file mode 100644 index 00000000..365ed6d5 --- /dev/null +++ b/ykman-rpc/ykman-rpc.py @@ -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) diff --git a/ykman-rpc/ykman-rpc.spec b/ykman-rpc/ykman-rpc.spec new file mode 100755 index 00000000..45b53191 --- /dev/null +++ b/ykman-rpc/ykman-rpc.spec @@ -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", +) diff --git a/ykman-rpc/yubikey-manager b/ykman-rpc/yubikey-manager new file mode 160000 index 00000000..ecd2500c --- /dev/null +++ b/ykman-rpc/yubikey-manager @@ -0,0 +1 @@ +Subproject commit ecd2500c8045c2deb89166cc58362e979639c3a9 diff --git a/yubikey-manager b/yubikey-manager deleted file mode 160000 index f90e4d6f..00000000 --- a/yubikey-manager +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f90e4d6f59e8acc4399e9ff587f8c821456d069c From 8362cc209302e0fc817ab35ad83d67f8cd3e5ab5 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Thu, 10 Feb 2022 13:36:38 +0100 Subject: [PATCH 2/4] Update build scripts. --- build-ykman.bat | 15 ++++++--------- build-ykman.sh | 21 +++++++++------------ linux/CMakeLists.txt | 4 ++-- windows/runner/CMakeLists.txt | 4 ++-- 4 files changed, 19 insertions(+), 25 deletions(-) diff --git a/build-ykman.bat b/build-ykman.bat index ee196f4a..3c79e4d3 100644 --- a/build-ykman.bat +++ b/build-ykman.bat @@ -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/ diff --git a/build-ykman.sh b/build-ykman.sh index 68456a0c..28df676b 100755 --- a/build-ykman.sh +++ b/build-ykman.sh @@ -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/" diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 5ad3f281..5e42a509 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -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) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt index 96d80c70..c08fdd6c 100644 --- a/windows/runner/CMakeLists.txt +++ b/windows/runner/CMakeLists.txt @@ -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") From bfbfc51a00dbc3b36ce56e3018ae5d271b60e603 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Thu, 10 Feb 2022 13:47:58 +0100 Subject: [PATCH 3/4] Use ykman-rpc from Flutter. --- lib/desktop/init.dart | 4 ++-- lib/desktop/rpc.dart | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/desktop/init.dart b/lib/desktop/init.dart index 7dc5c9fb..145c1326 100755 --- a/lib/desktop/init.dart +++ b/lib/desktop/init.dart @@ -28,7 +28,7 @@ Future> 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> 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 [ diff --git a/lib/desktop/rpc.dart b/lib/desktop/rpc.dart index a95104f6..3565fe24 100644 --- a/lib/desktop/rpc.dart +++ b/lib/desktop/rpc.dart @@ -78,8 +78,7 @@ class RpcSession { } static Future launch(String executable) async { - var process = - await Process.start(executable, [], environment: {'_YKMAN_RPC': '1'}); + var process = await Process.start(executable, []); return RpcSession(process); } From 84fe77f5008d2ac3107a99688f72f680c18ac8e8 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Thu, 10 Feb 2022 14:17:54 +0100 Subject: [PATCH 4/4] Update mac build for ykman-rpc. --- macos/Runner.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index a66407fb..21da19b2 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -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 = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - A549BDAA2747CBBE0016F37D /* ykman */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ykman; path = ../build/macos/ykman; sourceTree = ""; }; + A549BDAA2747CBBE0016F37D /* ykman-rpc */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "ykman-rpc"; path = "../build/macos/ykman-rpc"; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -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 */, );