mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-22 17:51:29 +03:00
Merge PR #38.
This commit is contained in:
commit
56bd68296a
2
.gitmodules
vendored
2
.gitmodules
vendored
@ -1,3 +1,3 @@
|
||||
[submodule "yubikey-manager"]
|
||||
path = yubikey-manager
|
||||
path = ykman-rpc/yubikey-manager
|
||||
url = https://github.com/Yubico/yubikey-manager
|
||||
|
@ -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/
|
||||
|
@ -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/"
|
||||
|
@ -28,7 +28,7 @@ Future<List<Override>> initializeAndGetOverrides() async {
|
||||
// Either use the _YKMAN_EXE environment variable, or look relative to executable.
|
||||
var exe = Platform.environment['_YKMAN_PATH'];
|
||||
if (exe?.isEmpty ?? true) {
|
||||
var relativePath = 'ykman/ykman';
|
||||
var relativePath = 'ykman-rpc/ykman-rpc';
|
||||
if (Platform.isMacOS) {
|
||||
relativePath = '../Resources/' + relativePath;
|
||||
} else if (Platform.isWindows) {
|
||||
@ -41,7 +41,7 @@ Future<List<Override>> initializeAndGetOverrides() async {
|
||||
|
||||
log.info('Starting subprocess: $exe');
|
||||
var rpc = await RpcSession.launch(exe!);
|
||||
log.info('ykman process started', exe);
|
||||
log.info('ykman-rpc process started', exe);
|
||||
rpc.setLogLevel(Logger.root.level);
|
||||
|
||||
return [
|
||||
|
@ -78,8 +78,7 @@ class RpcSession {
|
||||
}
|
||||
|
||||
static Future<RpcSession> launch(String executable) async {
|
||||
var process =
|
||||
await Process.start(executable, [], environment: {'_YKMAN_RPC': '1'});
|
||||
var process = await Process.start(executable, []);
|
||||
return RpcSession(process);
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -26,7 +26,7 @@
|
||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
||||
A549BDAB2747CBBE0016F37D /* ykman in Resources */ = {isa = PBXBuildFile; fileRef = A549BDAA2747CBBE0016F37D /* ykman */; };
|
||||
A549BDAB2747CBBE0016F37D /* ykman-rpc in Resources */ = {isa = PBXBuildFile; fileRef = A549BDAA2747CBBE0016F37D /* ykman-rpc */; };
|
||||
CCE73883AA6E76B42D34D392 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E5437883A25FD13EEA6A730E /* Pods_Runner.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@ -71,7 +71,7 @@
|
||||
6EAF9B998D311C2D6DD1409C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
||||
A549BDAA2747CBBE0016F37D /* ykman */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ykman; path = ../build/macos/ykman; sourceTree = "<group>"; };
|
||||
A549BDAA2747CBBE0016F37D /* ykman-rpc */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "ykman-rpc"; path = "../build/macos/ykman-rpc"; sourceTree = "<group>"; };
|
||||
E5437883A25FD13EEA6A730E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
F18D61C5361D1EF615E824EE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
FFD2BDD751CD366AEDC4D417 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
@ -122,7 +122,7 @@
|
||||
33CC11242044D66E0003C045 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A549BDAA2747CBBE0016F37D /* ykman */,
|
||||
A549BDAA2747CBBE0016F37D /* ykman-rpc */,
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */,
|
||||
33CC10F42044A3C60003C045 /* MainMenu.xib */,
|
||||
33CC10F72044A3C60003C045 /* Info.plist */,
|
||||
@ -248,7 +248,7 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A549BDAB2747CBBE0016F37D /* ykman in Resources */,
|
||||
A549BDAB2747CBBE0016F37D /* ykman-rpc in Resources */,
|
||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
|
||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
|
||||
);
|
||||
|
@ -17,5 +17,5 @@ target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||
|
||||
# This can probably be done in a cleaner way.
|
||||
file(COPY "../../build/windows/ykman" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/Release")
|
||||
file(COPY "../../build/windows/ykman" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/Debug")
|
||||
file(COPY "../../build/windows/ykman-rpc" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/Release")
|
||||
file(COPY "../../build/windows/ykman-rpc" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/Debug")
|
||||
|
3
ykman-rpc/.flake8
Normal file
3
ykman-rpc/.flake8
Normal file
@ -0,0 +1,3 @@
|
||||
[flake8]
|
||||
max-line-length = 88
|
||||
ignore = E203, W503
|
6
ykman-rpc/.gitignore
vendored
Normal file
6
ykman-rpc/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
*.pyc
|
||||
*.egg-info
|
||||
*.egg/
|
||||
.eggs/
|
||||
build/
|
||||
dist/
|
610
ykman-rpc/poetry.lock
generated
Normal file
610
ykman-rpc/poetry.lock
generated
Normal file
@ -0,0 +1,610 @@
|
||||
[[package]]
|
||||
name = "altgraph"
|
||||
version = "0.17.2"
|
||||
description = "Python graph (network) package"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "atomicwrites"
|
||||
version = "1.4.0"
|
||||
description = "Atomic file writes."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "21.4.0"
|
||||
description = "Classes Without Boilerplate"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[package.extras]
|
||||
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
|
||||
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
|
||||
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
|
||||
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.15.0"
|
||||
description = "Foreign Function Interface for Python calling C code."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
pycparser = "*"
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.0.3"
|
||||
description = "Composable command line interface toolkit"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.4"
|
||||
description = "Cross-platform colored terminal text."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "36.0.1"
|
||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
cffi = ">=1.12"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
|
||||
docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
|
||||
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
|
||||
sdist = ["setuptools_rust (>=0.11.4)"]
|
||||
ssh = ["bcrypt (>=3.1.5)"]
|
||||
test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "dataclasses"
|
||||
version = "0.8"
|
||||
description = "A backport of the dataclasses module for Python 3.6"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6, <3.7"
|
||||
|
||||
[[package]]
|
||||
name = "fido2"
|
||||
version = "0.9.3"
|
||||
description = "Python based FIDO 2.0 library"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7.6,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
|
||||
|
||||
[package.dependencies]
|
||||
cryptography = ">=1.5"
|
||||
six = "*"
|
||||
|
||||
[package.extras]
|
||||
pcsc = ["pyscard"]
|
||||
|
||||
[[package]]
|
||||
name = "future"
|
||||
version = "0.18.2"
|
||||
description = "Clean single-source support for Python 3 and 2"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "4.8.3"
|
||||
description = "Read metadata from Python packages"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
|
||||
zipp = ">=0.5"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
|
||||
perf = ["ipython"]
|
||||
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "1.1.1"
|
||||
description = "iniconfig: brain-dead simple config-ini parsing"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "jeepney"
|
||||
version = "0.7.1"
|
||||
description = "Low-level, pure Python DBus protocol wrapper."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio", "async-timeout"]
|
||||
trio = ["trio", "async-generator"]
|
||||
|
||||
[[package]]
|
||||
name = "keyring"
|
||||
version = "23.4.1"
|
||||
description = "Store and access your passwords safely."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
importlib-metadata = ">=3.6"
|
||||
jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""}
|
||||
pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""}
|
||||
SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"]
|
||||
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"]
|
||||
|
||||
[[package]]
|
||||
name = "macholib"
|
||||
version = "1.15.2"
|
||||
description = "Mach-O header analysis and editing"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
altgraph = ">=0.15"
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "21.3"
|
||||
description = "Core utilities for Python packages"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
|
||||
|
||||
[[package]]
|
||||
name = "pefile"
|
||||
version = "2021.9.3"
|
||||
description = "Python PE parsing module"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6.0"
|
||||
|
||||
[package.dependencies]
|
||||
future = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.0.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "py"
|
||||
version = "1.11.0"
|
||||
description = "library with cross-python path, ini-parsing, io, code, log facilities"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.21"
|
||||
description = "C parser in Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller"
|
||||
version = "4.9"
|
||||
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "<3.11,>=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
altgraph = "*"
|
||||
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
|
||||
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
|
||||
pefile = {version = ">=2017.8.1", markers = "sys_platform == \"win32\""}
|
||||
pyinstaller-hooks-contrib = ">=2020.6"
|
||||
pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""}
|
||||
|
||||
[package.extras]
|
||||
encryption = ["tinyaes (>=1.0.0)"]
|
||||
hook_testing = ["pytest (>=2.7.3)", "execnet (>=1.5.0)", "psutil"]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller-hooks-contrib"
|
||||
version = "2022.0"
|
||||
description = "Community maintained hooks for PyInstaller"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.0.7"
|
||||
description = "Python parsing module"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
diagrams = ["jinja2", "railroad-diagrams"]
|
||||
|
||||
[[package]]
|
||||
name = "pyscard"
|
||||
version = "2.0.2"
|
||||
description = "Smartcard module for Python."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.extras]
|
||||
Gui = ["wxpython"]
|
||||
Pyro = ["pyro"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.0.0"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
|
||||
attrs = ">=19.2.0"
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=0.12,<2.0"
|
||||
py = ">=1.8.2"
|
||||
tomli = ">=1.0.0"
|
||||
|
||||
[package.extras]
|
||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32"
|
||||
version = "303"
|
||||
description = "Python for Window Extensions"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pywin32-ctypes"
|
||||
version = "0.2.0"
|
||||
description = ""
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "secretstorage"
|
||||
version = "3.3.1"
|
||||
description = "Python bindings to FreeDesktop.org Secret Service API"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
cryptography = ">=2.0"
|
||||
jeepney = ">=0.6"
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.16.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "1.2.3"
|
||||
description = "A lil' TOML parser"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.0.1"
|
||||
description = "Backported and Experimental Type Hints for Python 3.6+"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "yubikey-manager"
|
||||
version = "4.1.0-dev0"
|
||||
description = "Tool for managing your YubiKey configuration."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "^3.6"
|
||||
develop = false
|
||||
|
||||
[package.dependencies]
|
||||
click = "^6.0 || ^7.0 || ^8.0"
|
||||
cryptography = ">=2.1, <39"
|
||||
dataclasses = {version = "^0.8", markers = "python_version < \"3.7\""}
|
||||
fido2 = ">=0.9, <1.0"
|
||||
keyring = "<23.5"
|
||||
pyscard = "^1.9 || ^2.0"
|
||||
pywin32 = {version = ">=223", markers = "sys_platform == \"win32\""}
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
url = "yubikey-manager"
|
||||
|
||||
[[package]]
|
||||
name = "zipp"
|
||||
version = "3.6.0"
|
||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
|
||||
testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.6"
|
||||
content-hash = "3ad372e77a86f3c7a25683cbeea3f2f98aeac8dbb279b7610429a62d23ba9325"
|
||||
|
||||
[metadata.files]
|
||||
altgraph = [
|
||||
{file = "altgraph-0.17.2-py2.py3-none-any.whl", hash = "sha256:743628f2ac6a7c26f5d9223c91ed8ecbba535f506f4b6f558885a8a56a105857"},
|
||||
{file = "altgraph-0.17.2.tar.gz", hash = "sha256:ebf2269361b47d97b3b88e696439f6e4cbc607c17c51feb1754f90fb79839158"},
|
||||
]
|
||||
atomicwrites = [
|
||||
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
|
||||
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
|
||||
]
|
||||
attrs = [
|
||||
{file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
|
||||
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
|
||||
]
|
||||
cffi = [
|
||||
{file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"},
|
||||
{file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"},
|
||||
{file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"},
|
||||
{file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"},
|
||||
{file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"},
|
||||
{file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"},
|
||||
{file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"},
|
||||
{file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"},
|
||||
{file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"},
|
||||
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"},
|
||||
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"},
|
||||
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"},
|
||||
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"},
|
||||
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"},
|
||||
{file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"},
|
||||
{file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"},
|
||||
{file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"},
|
||||
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"},
|
||||
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"},
|
||||
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"},
|
||||
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"},
|
||||
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"},
|
||||
{file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"},
|
||||
{file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"},
|
||||
{file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"},
|
||||
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"},
|
||||
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"},
|
||||
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"},
|
||||
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"},
|
||||
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"},
|
||||
{file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"},
|
||||
{file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"},
|
||||
{file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"},
|
||||
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"},
|
||||
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"},
|
||||
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"},
|
||||
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"},
|
||||
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"},
|
||||
{file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"},
|
||||
{file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"},
|
||||
{file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"},
|
||||
{file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"},
|
||||
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"},
|
||||
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"},
|
||||
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"},
|
||||
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"},
|
||||
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"},
|
||||
{file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"},
|
||||
{file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"},
|
||||
{file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"},
|
||||
]
|
||||
click = [
|
||||
{file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"},
|
||||
{file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"},
|
||||
]
|
||||
colorama = [
|
||||
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
|
||||
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
|
||||
]
|
||||
cryptography = [
|
||||
{file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b"},
|
||||
{file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31"},
|
||||
{file = "cryptography-36.0.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a"},
|
||||
{file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173"},
|
||||
{file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12"},
|
||||
{file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3"},
|
||||
{file = "cryptography-36.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2"},
|
||||
{file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f"},
|
||||
{file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3"},
|
||||
{file = "cryptography-36.0.1-cp36-abi3-win32.whl", hash = "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca"},
|
||||
{file = "cryptography-36.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf"},
|
||||
{file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9"},
|
||||
{file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1"},
|
||||
{file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94"},
|
||||
{file = "cryptography-36.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac"},
|
||||
{file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee"},
|
||||
{file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46"},
|
||||
{file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903"},
|
||||
{file = "cryptography-36.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316"},
|
||||
{file = "cryptography-36.0.1.tar.gz", hash = "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638"},
|
||||
]
|
||||
dataclasses = [
|
||||
{file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"},
|
||||
{file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"},
|
||||
]
|
||||
fido2 = [
|
||||
{file = "fido2-0.9.3.tar.gz", hash = "sha256:b45e89a6109cfcb7f1bb513776aa2d6408e95c4822f83a253918b944083466ec"},
|
||||
]
|
||||
future = [
|
||||
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
|
||||
]
|
||||
importlib-metadata = [
|
||||
{file = "importlib_metadata-4.8.3-py3-none-any.whl", hash = "sha256:65a9576a5b2d58ca44d133c42a241905cc45e34d2c06fd5ba2bafa221e5d7b5e"},
|
||||
{file = "importlib_metadata-4.8.3.tar.gz", hash = "sha256:766abffff765960fcc18003801f7044eb6755ffae4521c8e8ce8e83b9c9b0668"},
|
||||
]
|
||||
iniconfig = [
|
||||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||
]
|
||||
jeepney = [
|
||||
{file = "jeepney-0.7.1-py3-none-any.whl", hash = "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac"},
|
||||
{file = "jeepney-0.7.1.tar.gz", hash = "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f"},
|
||||
]
|
||||
keyring = [
|
||||
{file = "keyring-23.4.1-py3-none-any.whl", hash = "sha256:17e49fb0d6883c2b4445359434dba95aad84aabb29bbff044ad0ed7100232eca"},
|
||||
{file = "keyring-23.4.1.tar.gz", hash = "sha256:89cbd74d4683ed164c8082fb38619341097741323b3786905c6dac04d6915a55"},
|
||||
]
|
||||
macholib = [
|
||||
{file = "macholib-1.15.2-py2.py3-none-any.whl", hash = "sha256:885613dd02d3e26dbd2b541eb4cc4ce611b841f827c0958ab98656e478b9e6f6"},
|
||||
{file = "macholib-1.15.2.tar.gz", hash = "sha256:1542c41da3600509f91c165cb897e7e54c0e74008bd8da5da7ebbee519d593d2"},
|
||||
]
|
||||
packaging = [
|
||||
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
|
||||
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
|
||||
]
|
||||
pefile = [
|
||||
{file = "pefile-2021.9.3.tar.gz", hash = "sha256:344a49e40a94e10849f0fe34dddc80f773a12b40675bf2f7be4b8be578bdd94a"},
|
||||
]
|
||||
pluggy = [
|
||||
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
||||
]
|
||||
py = [
|
||||
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
|
||||
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
||||
]
|
||||
pycparser = [
|
||||
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
|
||||
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
|
||||
]
|
||||
pyinstaller = [
|
||||
{file = "pyinstaller-4.9-py3-none-macosx_10_13_universal2.whl", hash = "sha256:e2f165cea4470ce8a8349112cd78f48a61413805adc17792a91997a11cfe1d80"},
|
||||
{file = "pyinstaller-4.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:24035eb9fffa2e3e288b4c1c9710043819efc7203cae5c8c573bec16f4a8e98f"},
|
||||
{file = "pyinstaller-4.9-py3-none-manylinux2014_i686.whl", hash = "sha256:a0b988cfc197d40e3d773b3aa1c7d3e918fc0933b4c15ec3fc5d156f222d82cb"},
|
||||
{file = "pyinstaller-4.9-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:62c97cbbdbee30974d607eb1de9afb081eb3adba787c203b00438e21027b829b"},
|
||||
{file = "pyinstaller-4.9-py3-none-manylinux2014_s390x.whl", hash = "sha256:7f46ab11ec986e4c525b93251063144e12d432a132dbc0070e3030e34c76537a"},
|
||||
{file = "pyinstaller-4.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:b5f1a94150315ea75bf3501be6c8476d65a7209580bb662da06dbdbc4454f375"},
|
||||
{file = "pyinstaller-4.9-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:ebeb87cdbadb2b4e8f991ffd9945ebd4fb3a7303180e63682c3e1ce01b3fdd22"},
|
||||
{file = "pyinstaller-4.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:59372b950d176fdc5ecea29719a8ab3f194b73a15b7f9875ac2a1de9a3daf5ed"},
|
||||
{file = "pyinstaller-4.9-py3-none-win32.whl", hash = "sha256:ec3ca331d565ffca1b6470c5aaf798885a03708c3d0b15c1b19009126f84c1d4"},
|
||||
{file = "pyinstaller-4.9-py3-none-win_amd64.whl", hash = "sha256:bec57b3b2b6178907255557ec0fc4b5ce5a0474013414cdadea853205c74ed26"},
|
||||
{file = "pyinstaller-4.9.tar.gz", hash = "sha256:75a180a658871bc41f9cf94b6f90ffa54e98f5d6a7cdb02d7530f0360afe24f9"},
|
||||
]
|
||||
pyinstaller-hooks-contrib = [
|
||||
{file = "pyinstaller-hooks-contrib-2022.0.tar.gz", hash = "sha256:61b667f51b2525377fae30793f38fd9752a08032c72b209effabf707c840cc38"},
|
||||
{file = "pyinstaller_hooks_contrib-2022.0-py2.py3-none-any.whl", hash = "sha256:29f0bd8fbb2ff6f2df60a0c147e5b5ad65ae5c1a982d90641a5f712de03fa161"},
|
||||
]
|
||||
pyparsing = [
|
||||
{file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"},
|
||||
{file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"},
|
||||
]
|
||||
pyscard = [
|
||||
{file = "pyscard-2.0.2-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:eeeca096bc89feec311b44b9f2e9050157c6e87d9addada975e9ce479b3e39b7"},
|
||||
{file = "pyscard-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:07e4a091f1c75c3d426ae23c935cfa9e8bdd6860c99aeccdcc0eb28a895ec0b5"},
|
||||
{file = "pyscard-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:cfb2d8f05f0850637e25c21975b6a1ed01582308937a8c9e02e38d43f6495422"},
|
||||
{file = "pyscard-2.0.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:68773baf5aaf88a2a9862aa724e5d2fd43a75374e45cbb77beeb313d47fe3f31"},
|
||||
{file = "pyscard-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:3147be0097f88e18dca286e4a7aa58ac016c0a7a2f1b49f91ea42d7d46ac5fe8"},
|
||||
{file = "pyscard-2.0.2.tar.gz", hash = "sha256:05de0579c42b4eb433903aa2fb327d4821ebac262434b6584da18ed72053fd9e"},
|
||||
]
|
||||
pytest = [
|
||||
{file = "pytest-7.0.0-py3-none-any.whl", hash = "sha256:42901e6bd4bd4a0e533358a86e848427a49005a3256f657c5c8f8dd35ef137a9"},
|
||||
{file = "pytest-7.0.0.tar.gz", hash = "sha256:dad48ffda394e5ad9aa3b7d7ddf339ed502e5e365b1350e0af65f4a602344b11"},
|
||||
]
|
||||
pywin32 = [
|
||||
{file = "pywin32-303-cp310-cp310-win32.whl", hash = "sha256:6fed4af057039f309263fd3285d7b8042d41507343cd5fa781d98fcc5b90e8bb"},
|
||||
{file = "pywin32-303-cp310-cp310-win_amd64.whl", hash = "sha256:51cb52c5ec6709f96c3f26e7795b0bf169ee0d8395b2c1d7eb2c029a5008ed51"},
|
||||
{file = "pywin32-303-cp311-cp311-win32.whl", hash = "sha256:d9b5d87ca944eb3aa4cd45516203ead4b37ab06b8b777c54aedc35975dec0dee"},
|
||||
{file = "pywin32-303-cp311-cp311-win_amd64.whl", hash = "sha256:fcf44032f5b14fcda86028cdf49b6ebdaea091230eb0a757282aa656e4732439"},
|
||||
{file = "pywin32-303-cp36-cp36m-win32.whl", hash = "sha256:aad484d52ec58008ca36bd4ad14a71d7dd0a99db1a4ca71072213f63bf49c7d9"},
|
||||
{file = "pywin32-303-cp36-cp36m-win_amd64.whl", hash = "sha256:2a09632916b6bb231ba49983fe989f2f625cea237219530e81a69239cd0c4559"},
|
||||
{file = "pywin32-303-cp37-cp37m-win32.whl", hash = "sha256:b1675d82bcf6dbc96363fca747bac8bff6f6e4a447a4287ac652aa4b9adc796e"},
|
||||
{file = "pywin32-303-cp37-cp37m-win_amd64.whl", hash = "sha256:c268040769b48a13367221fced6d4232ed52f044ffafeda247bd9d2c6bdc29ca"},
|
||||
{file = "pywin32-303-cp38-cp38-win32.whl", hash = "sha256:5f9ec054f5a46a0f4dfd72af2ce1372f3d5a6e4052af20b858aa7df2df7d355b"},
|
||||
{file = "pywin32-303-cp38-cp38-win_amd64.whl", hash = "sha256:793bf74fce164bcffd9d57bb13c2c15d56e43c9542a7b9687b4fccf8f8a41aba"},
|
||||
{file = "pywin32-303-cp39-cp39-win32.whl", hash = "sha256:7d3271c98434617a11921c5ccf74615794d97b079e22ed7773790822735cc352"},
|
||||
{file = "pywin32-303-cp39-cp39-win_amd64.whl", hash = "sha256:79cbb862c11b9af19bcb682891c1b91942ec2ff7de8151e2aea2e175899cda34"},
|
||||
]
|
||||
pywin32-ctypes = [
|
||||
{file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"},
|
||||
{file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"},
|
||||
]
|
||||
secretstorage = [
|
||||
{file = "SecretStorage-3.3.1-py3-none-any.whl", hash = "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f"},
|
||||
{file = "SecretStorage-3.3.1.tar.gz", hash = "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195"},
|
||||
]
|
||||
six = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
tomli = [
|
||||
{file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"},
|
||||
{file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"},
|
||||
]
|
||||
typing-extensions = [
|
||||
{file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"},
|
||||
{file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"},
|
||||
]
|
||||
yubikey-manager = []
|
||||
zipp = [
|
||||
{file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"},
|
||||
{file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"},
|
||||
]
|
20
ykman-rpc/pyproject.toml
Normal file
20
ykman-rpc/pyproject.toml
Normal file
@ -0,0 +1,20 @@
|
||||
[tool.poetry]
|
||||
name = "ykman-rpc"
|
||||
version = "0.1.0"
|
||||
description = "Yubico Authenticator helper app"
|
||||
authors = ["Dain Nilsson <dain@yubico.com>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.6"
|
||||
yubikey-manager = {path = "yubikey-manager"}
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pyinstaller = {version = "^4.9", python = "<3.11"}
|
||||
pytest = "^7.0.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
231
ykman-rpc/rpc-shell.py
Executable file
231
ykman-rpc/rpc-shell.py
Executable file
@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import cmd
|
||||
import json
|
||||
import click
|
||||
import subprocess # nosec
|
||||
import sys
|
||||
|
||||
import logging
|
||||
from typing import IO, cast
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def red(value):
|
||||
return f"\u001b[31;1m{value}\u001b[0m"
|
||||
|
||||
|
||||
def green(value):
|
||||
return f"\u001b[32;1m{value}\u001b[0m"
|
||||
|
||||
|
||||
def yellow(value):
|
||||
return f"\u001b[33;1m{value}\u001b[0m"
|
||||
|
||||
|
||||
def cyan(value):
|
||||
return f"\u001b[36;1m{value}\u001b[0m"
|
||||
|
||||
|
||||
class RpcShell(cmd.Cmd):
|
||||
def __init__(self, stdin, stdout):
|
||||
super().__init__()
|
||||
self._stdin = stdin
|
||||
self._stdout = stdout
|
||||
self._echo = False
|
||||
self._path = []
|
||||
self._node = None
|
||||
self.do_cd(None)
|
||||
|
||||
def _send(self, data):
|
||||
if self._echo:
|
||||
print("SEND:", cyan(json.dumps(data)))
|
||||
json.dump(data, self._stdin)
|
||||
self._stdin.write("\n")
|
||||
self._stdin.flush()
|
||||
|
||||
def _recv(self):
|
||||
line = self._stdout.readline()
|
||||
if self._echo:
|
||||
print("RECV:", cyan(line))
|
||||
try:
|
||||
return json.loads(line)
|
||||
except Exception:
|
||||
print("failed to parse:", line)
|
||||
raise
|
||||
|
||||
@property
|
||||
def prompt(self):
|
||||
return "/" + "/".join(self._path) + "> "
|
||||
|
||||
def resolve_path(self, line):
|
||||
if line:
|
||||
parts = line.split("/")
|
||||
if parts[0]:
|
||||
parts = self._path + parts
|
||||
else:
|
||||
parts.pop(0)
|
||||
while ".." in parts:
|
||||
pos = parts.index("..")
|
||||
parts.pop(pos - 1)
|
||||
parts.pop(pos - 1)
|
||||
else:
|
||||
parts = self._path + [""]
|
||||
return parts
|
||||
|
||||
def completepath(self, text, nodes_only=False):
|
||||
target = self.resolve_path(text)
|
||||
cmd = target.pop() if target else ""
|
||||
node = self.get_node(target)
|
||||
if node:
|
||||
names = [n + "/" for n in node.get("children", [])]
|
||||
if not nodes_only:
|
||||
actions = node.get("actions", [])
|
||||
if "get" in actions:
|
||||
actions.remove("get")
|
||||
names += actions
|
||||
res = [n for n in names if n.startswith(cmd)]
|
||||
return res
|
||||
return []
|
||||
|
||||
def completedefault(self, cmd, text, *args):
|
||||
return self.completepath(text)
|
||||
|
||||
def completenames(self, cmd, text, *ignored):
|
||||
return self.completepath(text)
|
||||
|
||||
def emptyline(self):
|
||||
self.do_ls(None)
|
||||
|
||||
def get_node(self, target):
|
||||
logger.debug("sending get: %r", target)
|
||||
self._send({"kind": "command", "action": "get", "target": target})
|
||||
result = self._recv()
|
||||
logger.debug("got info: %r", result)
|
||||
kind = result["kind"]
|
||||
if kind == "success":
|
||||
return result
|
||||
elif kind == "error":
|
||||
status = result["status"]
|
||||
print(red(f"{status.upper()}: {result['body']}"))
|
||||
else:
|
||||
print(red(f"Invalid response: {result}"))
|
||||
|
||||
def do_echo(self, args):
|
||||
self._echo = not self._echo
|
||||
print("ECHO is", "on" if self._echo else "off")
|
||||
|
||||
def do_quit(self, args):
|
||||
return True
|
||||
|
||||
def do_cd(self, args):
|
||||
if args:
|
||||
target = self.resolve_path(args)
|
||||
if target and not target[-1]:
|
||||
target.pop()
|
||||
else:
|
||||
target = []
|
||||
logger.debug("Get info for %r", target)
|
||||
if self.get_node(target):
|
||||
self._path = target
|
||||
logger.debug("set path %r", target)
|
||||
|
||||
def complete_cd(self, cmd, text, *args):
|
||||
return self.completepath(text[3:], True)
|
||||
|
||||
def do_ls(self, args):
|
||||
self._send({"kind": "command", "action": "get", "target": self._path})
|
||||
result = self._recv()
|
||||
kind = result["kind"]
|
||||
if kind == "success":
|
||||
self._node = result["body"]
|
||||
data = self._node.get("data", None)
|
||||
if data:
|
||||
for k, v in data.items():
|
||||
print(yellow(f"{k}: {v}"))
|
||||
|
||||
for c, c_data in self._node.get("children", {}).items():
|
||||
print(green(f"{c}/"))
|
||||
if c_data:
|
||||
for k, v in c_data.items():
|
||||
print(yellow(f" {k}: {v}"))
|
||||
|
||||
for name in self._node.get("actions", []):
|
||||
if name != "get": # Don't show get, always available
|
||||
print(cyan(f"{name}"))
|
||||
elif kind == "error":
|
||||
status = result["status"]
|
||||
print(red(f"{status.upper()}: {result['body']}"))
|
||||
else:
|
||||
print(red(f"Invalid response: {result}"))
|
||||
|
||||
def default(self, line):
|
||||
parts = line.strip().split(maxsplit=1)
|
||||
if len(parts) == 2:
|
||||
try:
|
||||
args = json.loads(parts[1])
|
||||
if not isinstance(args, dict):
|
||||
logger.error("Argument must be a JSON Object")
|
||||
return
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error("Error decoding JSON.", exc_info=e)
|
||||
return
|
||||
else:
|
||||
args = {}
|
||||
target = self.resolve_path(parts[0])
|
||||
action = target.pop()
|
||||
self._send(
|
||||
{
|
||||
"kind": "command",
|
||||
"action": action or "get",
|
||||
"target": target,
|
||||
"body": args,
|
||||
}
|
||||
)
|
||||
|
||||
while True:
|
||||
result = self._recv()
|
||||
kind = result["kind"]
|
||||
|
||||
if kind == "signal":
|
||||
print(cyan(f"{result['status']}: {result.get('body', None)}"))
|
||||
else:
|
||||
break
|
||||
|
||||
if kind == "success":
|
||||
body = result.get("body", None)
|
||||
if body:
|
||||
print(yellow(json.dumps(body)))
|
||||
elif kind == "error":
|
||||
print(red(f"{result['status']}: {result['message']}"))
|
||||
body = result.get("body", None)
|
||||
if result:
|
||||
print(red(json.dumps(body)))
|
||||
else:
|
||||
print(red(f"Invalid response: {result}"))
|
||||
|
||||
def do_EOF(self, args):
|
||||
return True
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.argument("executable", nargs=-1)
|
||||
def shell(executable):
|
||||
"""A basic shell for interacting with the ykman rpc."""
|
||||
rpc = subprocess.Popen( # nosec
|
||||
executable or [sys.executable, "ykman-rpc.py"],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
encoding="utf8",
|
||||
)
|
||||
|
||||
click.echo("Shell starting...")
|
||||
shell = RpcShell(rpc.stdin, cast(IO[str], rpc.stdout))
|
||||
shell.cmdloop()
|
||||
click.echo("Stopping...")
|
||||
rpc.communicate()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
shell()
|
161
ykman-rpc/rpc/__init__.py
Normal file
161
ykman-rpc/rpc/__init__.py
Normal file
@ -0,0 +1,161 @@
|
||||
# Copyright (c) 2021 Yubico AB
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or
|
||||
# without modification, are permitted provided that the following
|
||||
# conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
from .base import RpcException, encode_bytes
|
||||
from .device import RootNode
|
||||
|
||||
from queue import Queue
|
||||
from threading import Thread, Event
|
||||
from typing import Callable, Dict, List
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _JsonLoggingFormatter(logging.Formatter):
|
||||
def format(self, record):
|
||||
data = {
|
||||
"time": record.created,
|
||||
"name": record.name,
|
||||
"level": record.levelname,
|
||||
"message": record.getMessage(),
|
||||
}
|
||||
if record.exc_info:
|
||||
if not record.exc_text:
|
||||
record.exc_text = self.formatException(record.exc_info)
|
||||
data["exc_text"] = record.exc_text
|
||||
return json.dumps(data)
|
||||
|
||||
|
||||
def _init_logging():
|
||||
logging.disable(logging.NOTSET)
|
||||
logging.basicConfig()
|
||||
logging.root.handlers[0].setFormatter(_JsonLoggingFormatter())
|
||||
|
||||
|
||||
def _handle_incoming(event, recv, error, cmd_queue):
|
||||
while True:
|
||||
request = recv()
|
||||
if not request:
|
||||
break
|
||||
try:
|
||||
kind = request["kind"]
|
||||
if kind == "signal":
|
||||
# Cancel signals are handled here, the rest forwarded
|
||||
if request["status"] == "cancel":
|
||||
event.set()
|
||||
else:
|
||||
# Ignore other signals
|
||||
logger.error("Unhandled signal: %r", request)
|
||||
elif kind == "command":
|
||||
cmd_queue.join() # Wait for existing command to complete
|
||||
event.clear() # Reset event for next command
|
||||
cmd_queue.put(request)
|
||||
else:
|
||||
error("invalid-command", "Unsupported request type")
|
||||
except KeyError as e:
|
||||
error("invalid-command", str(e))
|
||||
except RpcException as e:
|
||||
error(e.status, e.message, e.body)
|
||||
except Exception as e:
|
||||
error("exception", f"{e!r}")
|
||||
event.set()
|
||||
cmd_queue.put(None)
|
||||
|
||||
|
||||
def process(
|
||||
send: Callable[[Dict], None],
|
||||
recv: Callable[[], Dict],
|
||||
handler: Callable[[str, List, Dict, Event, Callable[[str], None]], Dict],
|
||||
) -> None:
|
||||
def error(status: str, message: str, body: Dict = {}):
|
||||
send(dict(kind="error", status=status, message=message, body=body))
|
||||
|
||||
def signal(status: str, body: Dict = {}):
|
||||
send(dict(kind="signal", status=status, body=body))
|
||||
|
||||
def success(body: Dict):
|
||||
send(dict(kind="success", body=body))
|
||||
|
||||
event = Event()
|
||||
cmd_queue: Queue = Queue(1)
|
||||
read_thread = Thread(target=_handle_incoming, args=(event, recv, error, cmd_queue))
|
||||
read_thread.start()
|
||||
|
||||
while True:
|
||||
request = cmd_queue.get()
|
||||
if request is None:
|
||||
break
|
||||
try:
|
||||
success(
|
||||
handler(
|
||||
request["action"],
|
||||
request.get("target", []),
|
||||
request.get("body", {}),
|
||||
event,
|
||||
signal,
|
||||
)
|
||||
)
|
||||
except RpcException as e:
|
||||
error(e.status, e.message, e.body)
|
||||
except Exception as e:
|
||||
error("exception", f"{e!r}")
|
||||
cmd_queue.task_done()
|
||||
|
||||
read_thread.join()
|
||||
|
||||
|
||||
def run_rpc(
|
||||
send: Callable[[Dict], None],
|
||||
recv: Callable[[], Dict],
|
||||
) -> None:
|
||||
process(send, recv, RootNode())
|
||||
|
||||
|
||||
def run_rpc_pipes(stdout, stdin):
|
||||
_init_logging()
|
||||
|
||||
def _json_encode(value):
|
||||
if isinstance(value, bytes):
|
||||
return encode_bytes(value)
|
||||
raise TypeError(type(value))
|
||||
|
||||
def send(data):
|
||||
json.dump(data, stdout, default=_json_encode)
|
||||
stdout.write("\n")
|
||||
stdout.flush()
|
||||
|
||||
def recv():
|
||||
line = stdin.readline()
|
||||
if line:
|
||||
return json.loads(line.strip())
|
||||
return None
|
||||
|
||||
run_rpc(send, recv)
|
204
ykman-rpc/rpc/base.py
Normal file
204
ykman-rpc/rpc/base.py
Normal file
@ -0,0 +1,204 @@
|
||||
# Copyright (c) 2021 Yubico AB
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or
|
||||
# without modification, are permitted provided that the following
|
||||
# conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
from functools import partial
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def encode_bytes(value: bytes) -> str:
|
||||
return value.hex()
|
||||
|
||||
|
||||
decode_bytes = bytes.fromhex
|
||||
|
||||
|
||||
class RpcException(Exception):
|
||||
"""An exception that is returned as the result of an RPC command.i
|
||||
|
||||
Types:
|
||||
invalid-command
|
||||
state-reset
|
||||
exception
|
||||
"""
|
||||
|
||||
def __init__(self, status, message, body=None):
|
||||
self.status = status
|
||||
self.message = message
|
||||
self.body = body or {}
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class InvalidParametersException(RpcException):
|
||||
def __init__(self, message):
|
||||
super().__init__("invalid-command", f"Invalid parameters: {message}")
|
||||
|
||||
|
||||
class NoSuchActionException(RpcException):
|
||||
def __init__(self, name):
|
||||
super().__init__("invalid-command", f"No such action: {name}")
|
||||
|
||||
|
||||
class NoSuchNodeException(RpcException):
|
||||
def __init__(self, name):
|
||||
super().__init__("invalid-command", f"No such node: {name}")
|
||||
|
||||
|
||||
class StateResetException(RpcException):
|
||||
def __init__(self, message, path):
|
||||
super().__init__(
|
||||
"state-reset", message or "State reset in node", dict(path=path)
|
||||
)
|
||||
|
||||
|
||||
class TimeoutException(RpcException):
|
||||
def __init__(self):
|
||||
super().__init__("timeout", "Command timed out waiting for user action")
|
||||
|
||||
|
||||
class ChildResetException(Exception):
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
super().__init__()
|
||||
|
||||
|
||||
MARKER_ACTION = "_rpc_action_marker"
|
||||
MARKER_CHILD = "_rpc_child_marker"
|
||||
|
||||
|
||||
def action(func=None, *, closes_child=True, condition=None):
|
||||
if not func:
|
||||
return partial(action, closes_child=closes_child, condition=condition)
|
||||
|
||||
setattr(func, MARKER_ACTION, dict(closes_child=closes_child, condition=condition))
|
||||
return func
|
||||
|
||||
|
||||
def child(func=None, *, condition=None):
|
||||
if not func:
|
||||
return partial(child, condition=condition)
|
||||
|
||||
setattr(func, MARKER_CHILD, dict(condition=condition))
|
||||
return func
|
||||
|
||||
|
||||
class RpcNode:
|
||||
def __init__(self):
|
||||
self._child = None
|
||||
self._child_name = None
|
||||
|
||||
def __call__(self, action, target, params, event, signal, traversed=None):
|
||||
traversed = traversed or []
|
||||
try:
|
||||
if target:
|
||||
traversed += [target[0]]
|
||||
return self.get_child(target[0])(
|
||||
action, target[1:], params, event, signal, traversed
|
||||
)
|
||||
if action in self.list_actions():
|
||||
return self.get_action(action)(params, event, signal)
|
||||
if action in self.list_children():
|
||||
traversed += [action]
|
||||
return self.get_child(action)(
|
||||
"get", [], params, event, signal, traversed
|
||||
)
|
||||
except ChildResetException as e:
|
||||
self._close_child()
|
||||
raise StateResetException(e.message, traversed)
|
||||
except ValueError as e:
|
||||
raise InvalidParametersException(e)
|
||||
raise NoSuchActionException(action)
|
||||
|
||||
def close(self):
|
||||
if self._child:
|
||||
self._close_child()
|
||||
|
||||
def get_data(self):
|
||||
return dict()
|
||||
|
||||
def _list_marked(self, marker):
|
||||
children = {}
|
||||
for name in dir(self):
|
||||
options = getattr(getattr(self, name), marker, None)
|
||||
if options is not None:
|
||||
condition = options["condition"]
|
||||
if condition is None or condition(self):
|
||||
children[name] = options
|
||||
return children
|
||||
|
||||
def list_actions(self):
|
||||
return list(self._list_marked(MARKER_ACTION))
|
||||
|
||||
def get_action(self, name):
|
||||
action = getattr(self, name, None)
|
||||
options = getattr(action, MARKER_ACTION, None)
|
||||
if options is not None:
|
||||
if options["closes_child"] and self._child:
|
||||
self._close_child()
|
||||
return action
|
||||
raise NoSuchActionException(name)
|
||||
|
||||
def list_children(self):
|
||||
return {name: {} for name in self._list_marked(MARKER_CHILD).keys()}
|
||||
|
||||
def create_child(self, name):
|
||||
child = getattr(self, name, None)
|
||||
if hasattr(child, MARKER_CHILD):
|
||||
return child()
|
||||
raise NoSuchNodeException(name)
|
||||
|
||||
def _close_child(self):
|
||||
if self._child:
|
||||
logger.debug("close existing child: %s", self._child_name)
|
||||
try:
|
||||
self._child.close()
|
||||
except Exception as e:
|
||||
logger.error("Error closing child", exc_info=e)
|
||||
self._child = None
|
||||
self._child_name = None
|
||||
|
||||
def get_child(self, name):
|
||||
if self._child and self._child_name != name:
|
||||
self._close_child()
|
||||
|
||||
if not self._child:
|
||||
self._child = self.create_child(name)
|
||||
self._child_name = name
|
||||
logger.debug("created child: %s", name)
|
||||
|
||||
return self._child
|
||||
|
||||
@action
|
||||
def get(self, params, event, signal):
|
||||
return dict(
|
||||
data=self.get_data(),
|
||||
actions=self.list_actions(),
|
||||
children=self.list_children(),
|
||||
)
|
348
ykman-rpc/rpc/device.py
Normal file
348
ykman-rpc/rpc/device.py
Normal file
@ -0,0 +1,348 @@
|
||||
# Copyright (c) 2021 Yubico AB
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or
|
||||
# without modification, are permitted provided that the following
|
||||
# conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
from .base import RpcNode, child, action, NoSuchNodeException, ChildResetException
|
||||
from .oath import OathNode
|
||||
from .fido import Ctap2Node
|
||||
from .yubiotp import YubiOtpNode
|
||||
from .management import ManagementNode
|
||||
from ykman import __version__ as ykman_version
|
||||
from ykman.base import PID
|
||||
from ykman.device import (
|
||||
scan_devices,
|
||||
list_all_devices,
|
||||
get_name,
|
||||
read_info,
|
||||
)
|
||||
from ykman.diagnostics import get_diagnostics
|
||||
from yubikit.core import TRANSPORT
|
||||
from yubikit.core.smartcard import SmartCardConnection, ApduError, SW
|
||||
from yubikit.core.otp import OtpConnection
|
||||
from yubikit.core.fido import FidoConnection
|
||||
from yubikit.management import CAPABILITY
|
||||
|
||||
from ykman.pcsc import list_devices, YK_READER_NAME
|
||||
from smartcard.Exceptions import SmartcardException
|
||||
from dataclasses import asdict
|
||||
from typing import Mapping, Tuple
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RootNode(RpcNode):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._devices = DevicesNode()
|
||||
self._readers = ReadersNode()
|
||||
|
||||
def __call__(self, *args):
|
||||
result = super().__call__(*args)
|
||||
if result is None:
|
||||
result = {}
|
||||
return result
|
||||
|
||||
def get_child(self, name):
|
||||
self._child = self.create_child(name)
|
||||
self._child_name = name
|
||||
return self._child
|
||||
|
||||
def get_data(self):
|
||||
return dict(version=ykman_version)
|
||||
|
||||
@child
|
||||
def usb(self):
|
||||
return self._devices
|
||||
|
||||
@child
|
||||
def nfc(self):
|
||||
return self._readers
|
||||
|
||||
@action
|
||||
def diagnose(self, *ignored):
|
||||
return dict(diagnostics=get_diagnostics())
|
||||
|
||||
@action(closes_child=False)
|
||||
def logging(self, params, event, signal):
|
||||
level = params["level"].upper()
|
||||
log_level_value = getattr(logging, level)
|
||||
logging.getLogger().setLevel(log_level_value)
|
||||
logger.info(f"Log level set to: {level}")
|
||||
return dict()
|
||||
|
||||
|
||||
class ReadersNode(RpcNode):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._state = set()
|
||||
self._readers = {}
|
||||
self._reader_mapping = {}
|
||||
|
||||
@action(closes_child=False)
|
||||
def scan(self, *ignored):
|
||||
return self.list_children()
|
||||
|
||||
def list_children(self):
|
||||
devices = [
|
||||
d for d in list_devices("") if YK_READER_NAME not in d.reader.name.lower()
|
||||
]
|
||||
state = {d.reader.name for d in devices}
|
||||
if self._state != state:
|
||||
self._readers = {}
|
||||
self._reader_mapping = {}
|
||||
for device in devices:
|
||||
dev_id = os.urandom(4).hex()
|
||||
self._reader_mapping[dev_id] = device
|
||||
self._readers[dev_id] = dict(name=device.reader.name)
|
||||
self._state = state
|
||||
return self._readers
|
||||
|
||||
def create_child(self, name):
|
||||
return ReaderDeviceNode(self._reader_mapping[name], None)
|
||||
|
||||
|
||||
class _ScanDevices:
|
||||
def __init__(self):
|
||||
self._state: Tuple[Mapping[PID, int], int] = ({}, 0)
|
||||
self._caching = False
|
||||
|
||||
def __call__(self):
|
||||
if not self._caching or not self._state[1]:
|
||||
self._state = scan_devices()
|
||||
return self._state
|
||||
|
||||
def __enter__(self):
|
||||
self._caching = True
|
||||
self._state = ({}, 0)
|
||||
|
||||
def __exit__(self, exc_type, exc, exc_tb):
|
||||
self._caching = False
|
||||
|
||||
|
||||
class DevicesNode(RpcNode):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._get_state = _ScanDevices()
|
||||
self._list_state = 0
|
||||
self._devices = {}
|
||||
self._device_mapping = {}
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
with self._get_state:
|
||||
return super().__call__(*args, **kwargs)
|
||||
|
||||
@action(closes_child=False)
|
||||
def scan(self, *ignored):
|
||||
return self.get_data()
|
||||
|
||||
def get_data(self):
|
||||
state = self._get_state()
|
||||
return dict(state=state[1], pids=state[0])
|
||||
|
||||
def list_children(self):
|
||||
state = self._get_state()
|
||||
if state[1] != self._list_state:
|
||||
self._devices = {}
|
||||
self._device_mapping = {}
|
||||
for dev, info in list_all_devices():
|
||||
dev_id = str(info.serial) if info.serial else os.urandom(4).hex()
|
||||
while dev_id in self._device_mapping:
|
||||
dev_id = os.urandom(4).hex()
|
||||
self._device_mapping[dev_id] = (dev, info)
|
||||
name = get_name(info, dev.pid.get_type() if dev.pid else None)
|
||||
self._devices[dev_id] = dict(pid=dev.pid, name=name, serial=info.serial)
|
||||
if len(state[0]) == len(self._devices):
|
||||
self._list_state = state[1]
|
||||
else:
|
||||
logger.warning("Not all devices identified")
|
||||
self._list_state = 0
|
||||
|
||||
return self._devices
|
||||
|
||||
def create_child(self, name):
|
||||
return UsbDeviceNode(*self._device_mapping[name])
|
||||
|
||||
|
||||
class AbstractDeviceNode(RpcNode):
|
||||
def __init__(self, device, info):
|
||||
super().__init__()
|
||||
self._device = device
|
||||
self._info = info
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
try:
|
||||
return super().__call__(*args, **kwargs)
|
||||
except (SmartcardException, OSError) as e:
|
||||
logger.error("Device error", exc_info=e)
|
||||
self._child = None
|
||||
name = self._child_name
|
||||
self._child_name = None
|
||||
raise NoSuchNodeException(name)
|
||||
|
||||
def create_child(self, name):
|
||||
try:
|
||||
return super().create_child(name)
|
||||
except (SmartcardException, OSError) as e:
|
||||
logger.error(f"Unable to create child {name}", exc_info=e)
|
||||
raise NoSuchNodeException(name)
|
||||
|
||||
def get_data(self):
|
||||
for conn_type in (SmartCardConnection, OtpConnection, FidoConnection):
|
||||
if self._device.supports_connection(conn_type):
|
||||
with self._device.open_connection(conn_type) as conn:
|
||||
pid = self._device.pid
|
||||
self._info = read_info(pid, conn)
|
||||
name = get_name(self._info, pid.get_type() if pid else None)
|
||||
return dict(
|
||||
pid=pid,
|
||||
name=name,
|
||||
transport=self._device.transport,
|
||||
info=asdict(self._info),
|
||||
)
|
||||
raise ValueError("No supported connections")
|
||||
|
||||
|
||||
class UsbDeviceNode(AbstractDeviceNode):
|
||||
def __init__(self, device, info):
|
||||
super().__init__(device, info)
|
||||
|
||||
def _supports_connection(self, conn_type):
|
||||
return self._device.supports_connection(conn_type)
|
||||
|
||||
def _create_connection(self, conn_type):
|
||||
connection = self._device.open_connection(conn_type)
|
||||
return ConnectionNode(self._device.transport, connection, self._info)
|
||||
|
||||
@child(condition=lambda self: self._supports_connection(SmartCardConnection))
|
||||
def ccid(self):
|
||||
return self._create_connection(SmartCardConnection)
|
||||
|
||||
@child(condition=lambda self: self._supports_connection(OtpConnection))
|
||||
def otp(self):
|
||||
return self._create_connection(OtpConnection)
|
||||
|
||||
@child(condition=lambda self: self._supports_connection(FidoConnection))
|
||||
def fido(self):
|
||||
return self._create_connection(FidoConnection)
|
||||
|
||||
|
||||
class ReaderDeviceNode(AbstractDeviceNode):
|
||||
def get_data(self):
|
||||
try:
|
||||
return super().get_data() | dict(present=True)
|
||||
except Exception:
|
||||
return dict(present=False)
|
||||
|
||||
@child
|
||||
def ccid(self):
|
||||
connection = self._device.open_connection(SmartCardConnection)
|
||||
info = read_info(None, connection)
|
||||
return ConnectionNode(self._device.transport, connection, info)
|
||||
|
||||
@child
|
||||
def fido(self):
|
||||
with self._device.open_connection(SmartCardConnection) as conn:
|
||||
info = read_info(None, conn)
|
||||
connection = self._device.open_connection(FidoConnection)
|
||||
return ConnectionNode(self._device.transport, connection, info)
|
||||
|
||||
|
||||
class ConnectionNode(RpcNode):
|
||||
def __init__(self, transport, connection, info):
|
||||
super().__init__()
|
||||
self._transport = transport
|
||||
self._connection = connection
|
||||
self._info = info or read_info(None, self._connection)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
try:
|
||||
return super().__call__(*args, **kwargs)
|
||||
except (SmartcardException, OSError) as e:
|
||||
logger.error("Connection error", exc_info=e)
|
||||
raise ChildResetException(f"{e}")
|
||||
except ApduError as e:
|
||||
if e.sw == SW.INVALID_INSTRUCTION:
|
||||
raise ChildResetException(f"SW: {e.sw}")
|
||||
raise e
|
||||
|
||||
@property
|
||||
def capabilities(self):
|
||||
return self._info.config.enabled_capabilities[self._transport]
|
||||
|
||||
def close(self):
|
||||
super().close()
|
||||
try:
|
||||
self._connection.close()
|
||||
except Exception as e:
|
||||
logger.warning("Error closing connection", exc_info=e)
|
||||
|
||||
def get_data(self):
|
||||
if (
|
||||
isinstance(self._connection, SmartCardConnection)
|
||||
or self._transport == TRANSPORT.USB
|
||||
):
|
||||
self._info = read_info(None, self._connection)
|
||||
return dict(version=self._info.version, serial=self._info.serial)
|
||||
|
||||
@child(
|
||||
condition=lambda self: self._transport == TRANSPORT.USB
|
||||
or isinstance(self._connection, SmartCardConnection)
|
||||
)
|
||||
def management(self):
|
||||
return ManagementNode(self._connection)
|
||||
|
||||
@child(
|
||||
condition=lambda self: isinstance(self._connection, SmartCardConnection)
|
||||
and CAPABILITY.OATH in self.capabilities
|
||||
)
|
||||
def oath(self):
|
||||
return OathNode(self._connection)
|
||||
|
||||
@child(
|
||||
condition=lambda self: isinstance(self._connection, FidoConnection)
|
||||
and CAPABILITY.FIDO2 in self.capabilities
|
||||
)
|
||||
def ctap2(self):
|
||||
return Ctap2Node(self._connection)
|
||||
|
||||
@child(
|
||||
condition=lambda self: CAPABILITY.OTP in self.capabilities
|
||||
and (
|
||||
isinstance(self._connection, OtpConnection)
|
||||
or ( # SmartCardConnection can be used over NFC, or on 5.3 and later.
|
||||
isinstance(self._connection, SmartCardConnection)
|
||||
and (
|
||||
self._transport == TRANSPORT.NFC or self._info.version >= (5, 3, 0)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
def yubiotp(self):
|
||||
return YubiOtpNode(self._connection)
|
247
ykman-rpc/rpc/fido.py
Normal file
247
ykman-rpc/rpc/fido.py
Normal file
@ -0,0 +1,247 @@
|
||||
# Copyright (c) 2021 Yubico AB
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or
|
||||
# without modification, are permitted provided that the following
|
||||
# conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
from .base import RpcNode, action, child
|
||||
from fido2.ctap2 import (
|
||||
Ctap2,
|
||||
ClientPin,
|
||||
CredentialManagement,
|
||||
FPBioEnrollment,
|
||||
CaptureError,
|
||||
)
|
||||
|
||||
|
||||
class Ctap2Node(RpcNode):
|
||||
def __init__(self, connection):
|
||||
super().__init__()
|
||||
self.ctap = Ctap2(connection)
|
||||
self._info = self.ctap.info
|
||||
self.client_pin = ClientPin(self.ctap)
|
||||
self._pin = None
|
||||
|
||||
def get_data(self):
|
||||
self._info = self.ctap.get_info()
|
||||
data = dict(info=self._info.data, locked=False)
|
||||
if self._info.options.get("clientPin"):
|
||||
data["locked"] = self._pin is None
|
||||
pin_retries, power_cycle = self.client_pin.get_pin_retries()
|
||||
data.update(
|
||||
pin_retries=pin_retries,
|
||||
power_cycle=power_cycle,
|
||||
)
|
||||
if self._info.options.get("bioEnroll"):
|
||||
uv_retries = self.client_pin.get_uv_retries()
|
||||
if isinstance(uv_retries, tuple):
|
||||
uv_retries = uv_retries[0]
|
||||
data.update(uv_retries=uv_retries)
|
||||
return data
|
||||
|
||||
@action
|
||||
def reset(self, params, event, signal):
|
||||
self.ctap.reset(event)
|
||||
self._pin = None
|
||||
return dict()
|
||||
|
||||
@action(condition=lambda self: self._info.options["clientPin"])
|
||||
def verify_pin(self, params, event, signal):
|
||||
pin = params.pop("pin")
|
||||
self.client_pin.get_pin_token(
|
||||
pin, ClientPin.PERMISSION.GET_ASSERTION, "ykman.example.com"
|
||||
)
|
||||
self._pin = pin
|
||||
|
||||
@action
|
||||
def set_pin(self, params, event, signal):
|
||||
has_pin = self.ctap.get_info().options["clientPin"]
|
||||
if has_pin:
|
||||
self.client_pin.change_pin(
|
||||
params.pop("pin"),
|
||||
params.pop("new_pin"),
|
||||
)
|
||||
else:
|
||||
self.client_pin.set_pin(
|
||||
params.pop("new_pin"),
|
||||
)
|
||||
self._pin = None
|
||||
|
||||
@child(condition=lambda self: "bioEnroll" in self._info.options and self._pin)
|
||||
def fingerprints(self):
|
||||
token = self.client_pin.get_pin_token(
|
||||
self._pin, ClientPin.PERMISSION.BIO_ENROLL
|
||||
)
|
||||
bio = FPBioEnrollment(self.ctap, self.client_pin.protocol, token)
|
||||
return FingerprintsNode(bio)
|
||||
|
||||
# TODO: Use CredentialManagement.is_supported when released
|
||||
@child(condition=lambda self: self._pin)
|
||||
def credentials(self):
|
||||
token = self.client_pin.get_pin_token(
|
||||
self._pin, ClientPin.PERMISSION.CREDENTIAL_MGMT
|
||||
)
|
||||
creds = CredentialManagement(self.ctap, self.client_pin.protocol, token)
|
||||
return CredentialsRpsNode(creds)
|
||||
|
||||
|
||||
class CredentialsRpsNode(RpcNode):
|
||||
def __init__(self, credman):
|
||||
super().__init__()
|
||||
self.credman = credman
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
data = self.credman.get_metadata()
|
||||
if data.get(CredentialManagement.RESULT.EXISTING_CRED_COUNT) == 0:
|
||||
self._rps = {}
|
||||
else:
|
||||
self._rps = {
|
||||
rp[CredentialManagement.RESULT.RP]["id"]: dict(
|
||||
rp_id=rp[CredentialManagement.RESULT.RP]["id"],
|
||||
rp_id_hash=rp[CredentialManagement.RESULT.RP_ID_HASH],
|
||||
)
|
||||
for rp in self.credman.enumerate_rps()
|
||||
}
|
||||
|
||||
def list_children(self):
|
||||
return self._rps
|
||||
|
||||
def create_child(self, name):
|
||||
if name in self._rps:
|
||||
return CredentialsRpNode(self.credman, self._rps[name], self.refresh)
|
||||
return super().create_child(name)
|
||||
|
||||
|
||||
class CredentialsRpNode(RpcNode):
|
||||
def __init__(self, credman, rp_data, refresh):
|
||||
super().__init__()
|
||||
self.credman = credman
|
||||
self.data = rp_data
|
||||
self.refresh_rps = refresh
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
self.refresh_rps()
|
||||
self._creds = {
|
||||
cred[CredentialManagement.RESULT.CREDENTIAL_ID]["id"].hex(): dict(
|
||||
credential_id=cred[CredentialManagement.RESULT.CREDENTIAL_ID],
|
||||
user_id=cred[CredentialManagement.RESULT.USER]["id"],
|
||||
user_name=cred[CredentialManagement.RESULT.USER]["name"],
|
||||
)
|
||||
for cred in self.credman.enumerate_creds(self.data["rp_id_hash"])
|
||||
}
|
||||
|
||||
def list_children(self):
|
||||
return self._creds
|
||||
|
||||
def create_child(self, name):
|
||||
if name in self._creds:
|
||||
return CredentialNode(
|
||||
self.credman,
|
||||
self._creds[name],
|
||||
self.refresh,
|
||||
)
|
||||
return super().create_child(name)
|
||||
|
||||
|
||||
class CredentialNode(RpcNode):
|
||||
def __init__(self, credman, credential_data, refresh):
|
||||
super().__init__()
|
||||
self.credman = credman
|
||||
self.data = credential_data
|
||||
self.refresh = refresh
|
||||
|
||||
def get_data(self):
|
||||
return self.data
|
||||
|
||||
@action
|
||||
def delete(self, params, event, signal):
|
||||
self.credman.delete_cred(self.data["credential_id"])
|
||||
self.refresh()
|
||||
|
||||
|
||||
class FingerprintsNode(RpcNode):
|
||||
def __init__(self, bio):
|
||||
super().__init__()
|
||||
self.bio = bio
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
self._templates = self.bio.enumerate_enrollments()
|
||||
|
||||
def list_children(self):
|
||||
return {
|
||||
template_id.hex(): dict(name=name)
|
||||
for template_id, name in self._templates.items()
|
||||
}
|
||||
|
||||
def create_child(self, name):
|
||||
template_id = bytes.fromhex(name)
|
||||
if template_id in self._templates:
|
||||
return FingerprintNode(
|
||||
self.bio, template_id, self._templates[template_id], self.refresh
|
||||
)
|
||||
return super().create_child(name)
|
||||
|
||||
@action
|
||||
def add(self, params, event, signal):
|
||||
name = params.get("name", None)
|
||||
enroller = self.bio.enroll()
|
||||
template_id = None
|
||||
while template_id is None:
|
||||
try:
|
||||
template_id = enroller.capture(event)
|
||||
signal("capture", dict(remaining=enroller.remaining))
|
||||
except CaptureError as e:
|
||||
signal("capture-error", dict(code=e.code))
|
||||
if name:
|
||||
self.bio.set_name(template_id, name)
|
||||
self._templates[template_id] = name
|
||||
return dict(template_id=template_id, name=name)
|
||||
|
||||
|
||||
class FingerprintNode(RpcNode):
|
||||
def __init__(self, bio, template_id, name, refresh):
|
||||
super().__init__()
|
||||
self.bio = bio
|
||||
self.refresh = refresh
|
||||
self.template_id = template_id
|
||||
self.name = name
|
||||
|
||||
def get_data(self):
|
||||
return dict(template_id=self.template_id, name=self.name)
|
||||
|
||||
@action
|
||||
def rename(self, params, event, signal):
|
||||
name = params.pop("name")
|
||||
self.bio.set_name(self.template_id, name)
|
||||
self.name = name
|
||||
self.refresh()
|
||||
|
||||
@action
|
||||
def delete(self, params, event, signal):
|
||||
self.bio.remove_enrollment(self.template_id)
|
||||
self.refresh()
|
73
ykman-rpc/rpc/management.py
Normal file
73
ykman-rpc/rpc/management.py
Normal file
@ -0,0 +1,73 @@
|
||||
# Copyright (c) 2021 Yubico AB
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or
|
||||
# without modification, are permitted provided that the following
|
||||
# conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
from .base import RpcNode, action
|
||||
from yubikit.core import require_version, NotSupportedError
|
||||
from yubikit.management import ManagementSession, DeviceConfig
|
||||
from dataclasses import asdict
|
||||
|
||||
|
||||
class ManagementNode(RpcNode):
|
||||
def __init__(self, connection):
|
||||
super().__init__()
|
||||
self.session = ManagementSession(connection)
|
||||
|
||||
def get_data(self):
|
||||
return asdict(self.session.read_device_info())
|
||||
|
||||
def list_actions(self):
|
||||
actions = super().list_actions()
|
||||
try:
|
||||
require_version(self.session.version, (5, 0, 0))
|
||||
actions.remove("set_mode")
|
||||
except NotSupportedError:
|
||||
actions.remove("configure")
|
||||
return actions
|
||||
|
||||
@action
|
||||
def configure(self, params, event, signal):
|
||||
reboot = params.pop("reboot", False)
|
||||
cur_lock_code = bytes.fromhex(params.pop("cur_lock_code", "")) or None
|
||||
new_lock_code = bytes.fromhex(params.pop("new_lock_code", "")) or None
|
||||
config = DeviceConfig(
|
||||
params.pop("enabled_capabilities", {}),
|
||||
params.pop("auto_eject_timeout", None),
|
||||
params.pop("challenge_response_timeout", None),
|
||||
params.pop("device_flags", None),
|
||||
)
|
||||
self.session.write_device_config(config, reboot, cur_lock_code, new_lock_code)
|
||||
return dict()
|
||||
|
||||
@action
|
||||
def set_mode(self, params, event, signal):
|
||||
self.session.set_mode(
|
||||
params.pop("mode"),
|
||||
params.pop("challenge_response_timeout", 0),
|
||||
params.pop("auto_eject_timeout", 0),
|
||||
)
|
||||
return dict()
|
300
ykman-rpc/rpc/oath.py
Normal file
300
ykman-rpc/rpc/oath.py
Normal file
@ -0,0 +1,300 @@
|
||||
# Copyright (c) 2021 Yubico AB
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or
|
||||
# without modification, are permitted provided that the following
|
||||
# conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
from .base import (
|
||||
RpcNode,
|
||||
action,
|
||||
child,
|
||||
ChildResetException,
|
||||
TimeoutException,
|
||||
RpcException,
|
||||
encode_bytes,
|
||||
decode_bytes,
|
||||
)
|
||||
from ykman.settings import AppData
|
||||
from yubikit.core import require_version, NotSupportedError
|
||||
from yubikit.core.smartcard import ApduError, SW
|
||||
from yubikit.oath import OathSession, CredentialData, OATH_TYPE, HASH_ALGORITHM
|
||||
from dataclasses import asdict
|
||||
from time import time
|
||||
import hmac
|
||||
import os
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AuthRequiredException(RpcException):
|
||||
def __init__(self):
|
||||
super().__init__("auth-required", "Authentication is required")
|
||||
|
||||
|
||||
def _get_keys():
|
||||
return AppData("oath_keys")
|
||||
|
||||
|
||||
class OathNode(RpcNode):
|
||||
def __init__(self, connection):
|
||||
super().__init__()
|
||||
self.session = OathSession(connection)
|
||||
|
||||
if self.session.locked:
|
||||
keys = _get_keys()
|
||||
if keys.keyring_available and self.session.device_id in keys:
|
||||
try:
|
||||
key = bytes.fromhex(keys.get_secret(self.session.device_id))
|
||||
self._do_validate(key)
|
||||
except ApduError as e:
|
||||
# Delete wrong key and fall through to prompt
|
||||
if e.sw == SW.INCORRECT_PARAMETERS:
|
||||
del keys[self.session.device_id]
|
||||
keys.write()
|
||||
except Exception as e:
|
||||
# Other error, fall though to prompt
|
||||
logger.warning("Error authenticating", exc_info=e)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
try:
|
||||
return super().__call__(*args, **kwargs)
|
||||
except ApduError as e:
|
||||
if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED:
|
||||
raise AuthRequiredException()
|
||||
# TODO: This should probably be in a baseclass of all "AppNodes".
|
||||
raise ChildResetException(f"SW: {e.sw:x}")
|
||||
|
||||
def get_data(self):
|
||||
keys = _get_keys()
|
||||
return dict(
|
||||
version=self.session.version,
|
||||
device_id=self.session.device_id,
|
||||
has_key=self.session.has_key,
|
||||
locked=self.session.locked,
|
||||
remembered=self.session.device_id in keys,
|
||||
)
|
||||
|
||||
@action
|
||||
def derive(self, params, event, signal):
|
||||
return dict(key=self.session.derive_key(params.pop("password")))
|
||||
|
||||
@action
|
||||
def forget(self, params, event, signal):
|
||||
keys = _get_keys()
|
||||
del keys[self.session.device_id]
|
||||
keys.write()
|
||||
return dict()
|
||||
|
||||
def _remember_key(self, key):
|
||||
keys = _get_keys()
|
||||
if key is None:
|
||||
if self.session.device_id in keys:
|
||||
del keys[self.session.device_id]
|
||||
keys.write()
|
||||
else:
|
||||
keys.put_secret(self.session.device_id, key.hex())
|
||||
keys.write()
|
||||
|
||||
def _get_key(self, params):
|
||||
has_key = "key" in params
|
||||
has_pw = "password" in params
|
||||
if has_key and has_pw:
|
||||
raise ValueError("Only one of 'key' and 'password' can be provided.")
|
||||
if has_pw:
|
||||
return self.session.derive_key(params.pop("password"))
|
||||
if has_key:
|
||||
return decode_bytes(params.pop("key"))
|
||||
raise ValueError("One of 'key' and 'password' must be provided.")
|
||||
|
||||
def _do_validate(self, key):
|
||||
self.session.validate(key)
|
||||
salt = os.urandom(32)
|
||||
digest = hmac.new(salt, key, "sha256").digest()
|
||||
self._key_verifier = (salt, digest)
|
||||
|
||||
@action
|
||||
def validate(self, params, event, signal):
|
||||
remember = params.pop("remember", False)
|
||||
key = self._get_key(params)
|
||||
if self.session.locked:
|
||||
try:
|
||||
self._do_validate(key)
|
||||
if remember:
|
||||
self._remember_key(key)
|
||||
result = True
|
||||
except ApduError as e:
|
||||
if e.sw == SW.INCORRECT_PARAMETERS:
|
||||
return dict(success=False)
|
||||
raise e
|
||||
elif hasattr(self, "_key_verifier"):
|
||||
salt, digest = self._key_verifier
|
||||
verify = hmac.new(salt, key, "sha256").digest()
|
||||
result = hmac.compare_digest(digest, verify)
|
||||
else:
|
||||
result = False
|
||||
return dict(success=result)
|
||||
|
||||
@action
|
||||
def set_key(self, params, event, signal):
|
||||
remember = params.pop("remember", False)
|
||||
key = self._get_key(params)
|
||||
self.session.set_key(key)
|
||||
self._remember_key(key if remember else None)
|
||||
return dict()
|
||||
|
||||
@action(condition=lambda self: self.session.has_key)
|
||||
def unset_key(self, params, event, signal):
|
||||
self.session.unset_key()
|
||||
self._remember_key(None)
|
||||
return dict()
|
||||
|
||||
@action
|
||||
def reset(self, params, event, signal):
|
||||
self.session.reset()
|
||||
self._remember_key(None)
|
||||
return dict()
|
||||
|
||||
@child(condition=lambda self: not self.session.locked)
|
||||
def accounts(self):
|
||||
return CredentialsNode(self.session)
|
||||
|
||||
|
||||
class CredentialsNode(RpcNode):
|
||||
def __init__(self, session):
|
||||
super().__init__()
|
||||
self.session = session
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
# N.B. We use 'calculate_all' since it tells us if a TOTP credential
|
||||
# requires touch or not.
|
||||
self._creds = {c.id: c for c in self.session.calculate_all().keys()}
|
||||
if self._child and self._child_name not in self._creds:
|
||||
self._close_child()
|
||||
|
||||
def list_children(self):
|
||||
return {encode_bytes(c_id): asdict(c) for c_id, c in self._creds.items()}
|
||||
|
||||
def create_child(self, name):
|
||||
key = decode_bytes(name)
|
||||
if key in self._creds:
|
||||
return CredentialNode(self.session, self._creds[key], self.refresh)
|
||||
return super().create_child(name)
|
||||
|
||||
@action
|
||||
def calculate_all(self, params, event, signal):
|
||||
timestamp = params.pop("timestamp", None)
|
||||
result = self.session.calculate_all(timestamp)
|
||||
return dict(
|
||||
entries=[
|
||||
dict(credential=asdict(cred), code=(asdict(code) if code else None))
|
||||
for (cred, code) in result.items()
|
||||
]
|
||||
)
|
||||
|
||||
@action
|
||||
def put(self, params, event, signal):
|
||||
require_touch = params.pop("require_touch", False)
|
||||
if "uri" in params:
|
||||
data = CredentialData.parse_uri(params.pop("uri"))
|
||||
if params:
|
||||
raise ValueError("Unsupported parameters present")
|
||||
else:
|
||||
data = CredentialData(
|
||||
params.pop("name"),
|
||||
OATH_TYPE[params.pop("oath_type").upper()],
|
||||
HASH_ALGORITHM[params.pop("hash", "sha1".upper())],
|
||||
decode_bytes(params.pop("secret")),
|
||||
**params,
|
||||
)
|
||||
|
||||
if data.get_id() in self._creds:
|
||||
raise ValueError("Credential already exists")
|
||||
credential = self.session.put_credential(data, require_touch)
|
||||
self._creds[credential.id] = credential
|
||||
return asdict(credential)
|
||||
|
||||
|
||||
class CredentialNode(RpcNode):
|
||||
def __init__(self, session, credential, refresh):
|
||||
super().__init__()
|
||||
self.session = session
|
||||
self.credential = credential
|
||||
self.refresh = refresh
|
||||
|
||||
def _require_version(self, major, minor, micro):
|
||||
try:
|
||||
require_version(self.session.version, (major, minor, micro))
|
||||
return True
|
||||
except NotSupportedError:
|
||||
return False
|
||||
|
||||
def get_info(self):
|
||||
return asdict(self.credential)
|
||||
|
||||
@action
|
||||
def code(self, params, event, signal):
|
||||
timestamp = params.pop("timestamp", None)
|
||||
try:
|
||||
start = time()
|
||||
code = self.session.calculate_code(self.credential, timestamp)
|
||||
return asdict(code)
|
||||
except ApduError as e:
|
||||
if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED and time() - start > 5:
|
||||
raise TimeoutException()
|
||||
raise
|
||||
|
||||
@action
|
||||
def calculate(self, params, event, signal):
|
||||
challenge = decode_bytes(params.pop("challenge"))
|
||||
try:
|
||||
start = time()
|
||||
response = self.session.calculate(self.credential.id, challenge)
|
||||
return dict(response=response)
|
||||
except ApduError as e:
|
||||
if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED and time() - start > 5:
|
||||
raise TimeoutException()
|
||||
raise
|
||||
|
||||
@action
|
||||
def delete(self, params, event, signal):
|
||||
self.session.delete_credential(self.credential.id)
|
||||
self.refresh()
|
||||
self.credential = None
|
||||
return dict()
|
||||
|
||||
@action(condition=lambda self: self._require_version(5, 3, 1))
|
||||
def rename(self, params, event, signal):
|
||||
name = params.pop("name")
|
||||
issuer = params.pop("issuer", None)
|
||||
try:
|
||||
new_id = self.session.rename_credential(self.credential.id, name, issuer)
|
||||
self.refresh()
|
||||
return dict(credential_id=new_id)
|
||||
except ApduError as e:
|
||||
if e.sw == SW.INCORRECT_PARAMETERS:
|
||||
raise ValueError("Issuer/name too long")
|
||||
raise e
|
175
ykman-rpc/rpc/yubiotp.py
Normal file
175
ykman-rpc/rpc/yubiotp.py
Normal file
@ -0,0 +1,175 @@
|
||||
# Copyright (c) 2021 Yubico AB
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or
|
||||
# without modification, are permitted provided that the following
|
||||
# conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
from .base import RpcNode, action, child
|
||||
|
||||
from yubikit.yubiotp import (
|
||||
YubiOtpSession,
|
||||
SLOT,
|
||||
UpdateConfiguration,
|
||||
HmacSha1SlotConfiguration,
|
||||
HotpSlotConfiguration,
|
||||
StaticPasswordSlotConfiguration,
|
||||
YubiOtpSlotConfiguration,
|
||||
StaticTicketSlotConfiguration,
|
||||
)
|
||||
|
||||
|
||||
class YubiOtpNode(RpcNode):
|
||||
def __init__(self, connection):
|
||||
super().__init__()
|
||||
self.session = YubiOtpSession(connection)
|
||||
|
||||
def get_data(self):
|
||||
state = self.session.get_config_state()
|
||||
data = dict(
|
||||
is_led_inverted=state.is_led_inverted(),
|
||||
slot1_configured=state.is_configured(SLOT.ONE),
|
||||
slot2_configured=state.is_configured(SLOT.TWO),
|
||||
)
|
||||
if self.session.version >= (3, 0, 0):
|
||||
data.update(
|
||||
slot1_touch_triggered=state.is_touch_triggered(SLOT.ONE),
|
||||
slot2_touch_triggered=state.is_touch_triggered(SLOT.TWO),
|
||||
)
|
||||
return data
|
||||
|
||||
@action
|
||||
def swap(self, params, event, signal):
|
||||
self.session.swap_slots()
|
||||
return dict()
|
||||
|
||||
@child
|
||||
def one(self):
|
||||
return SlotNode(self.session, SLOT.ONE)
|
||||
|
||||
@child
|
||||
def two(self):
|
||||
return SlotNode(self.session, SLOT.TWO)
|
||||
|
||||
|
||||
_CONFIG_TYPES = dict(
|
||||
hmac_sha1=HmacSha1SlotConfiguration,
|
||||
hotp=HotpSlotConfiguration,
|
||||
static_password=StaticPasswordSlotConfiguration,
|
||||
yubiotp=YubiOtpSlotConfiguration,
|
||||
static_ticket=StaticTicketSlotConfiguration,
|
||||
)
|
||||
|
||||
|
||||
class SlotNode(RpcNode):
|
||||
def __init__(self, session, slot):
|
||||
super().__init__()
|
||||
self.session = session
|
||||
self.slot = slot
|
||||
self._state = self.session.get_config_state()
|
||||
|
||||
def get_data(self):
|
||||
self._state = self.session.get_config_state()
|
||||
data = dict(is_configured=self._state.is_configured(self.slot))
|
||||
if self.session.version >= (3, 0, 0):
|
||||
data.update(is_touch_triggered=self._state.is_touch_triggered(self.slot))
|
||||
return data
|
||||
|
||||
@action(condition=lambda self: self._state.is_configured(self.slot))
|
||||
def delete(self, params, event, signal):
|
||||
self.session.delete_slot(self.slot, params.pop("cur_acc_code", None))
|
||||
|
||||
@action(
|
||||
condition=lambda self: self._state.is_configured(self.slot)
|
||||
and not self._state.is_touch_triggered(self.slot)
|
||||
)
|
||||
def calculate(self, params, event, signal):
|
||||
challenge = bytes.fromhex(params.pop("challenge"))
|
||||
response = self.session.calculate_hmac_sha1(self.slot, challenge, event)
|
||||
return dict(response=response)
|
||||
|
||||
def _apply_config(self, config, params):
|
||||
for option in (
|
||||
"serial_api_visible",
|
||||
"serial_usb_visible",
|
||||
"allow_update",
|
||||
"dormant",
|
||||
"invert_led",
|
||||
"protect_slot2",
|
||||
"require_touch",
|
||||
"lt64",
|
||||
"append_cr",
|
||||
"use_numeric",
|
||||
"fast_trigger",
|
||||
"digits8",
|
||||
"imf",
|
||||
"send_reference",
|
||||
"short_ticket",
|
||||
"manual_update",
|
||||
):
|
||||
if option in params:
|
||||
getattr(config, option)(params.pop(option))
|
||||
|
||||
for option in ("tabs", "delay", "pacing", "strong_password"):
|
||||
if option in params:
|
||||
getattr(config, option)(*params.pop(option))
|
||||
|
||||
if "token_id" in params:
|
||||
token_id, *args = params.pop("token_id")
|
||||
config.token_id(bytes.fromhex(token_id), *args)
|
||||
|
||||
return config
|
||||
|
||||
@action
|
||||
def put(self, params, event, signal):
|
||||
config = None
|
||||
for key in _CONFIG_TYPES:
|
||||
if key in params:
|
||||
if config is not None:
|
||||
raise ValueError("Only one configuration type can be provided.")
|
||||
config = _CONFIG_TYPES[key](
|
||||
*(bytes.fromhex(arg) for arg in params.pop(key))
|
||||
)
|
||||
if config is None:
|
||||
raise ValueError("No supported configuration type provided.")
|
||||
self._apply_config(config, params)
|
||||
self.session.put_configuration(
|
||||
self.slot,
|
||||
config,
|
||||
params.pop("acc_code", None),
|
||||
params.pop("cur_acc_code", None),
|
||||
)
|
||||
return dict()
|
||||
|
||||
@action(condition=lambda self: self._state.is_configured(self.slot))
|
||||
def update(self, params, event, signal):
|
||||
config = UpdateConfiguration()
|
||||
self._apply_config(config, params)
|
||||
self.session.update_configuration(
|
||||
self.slot,
|
||||
config,
|
||||
params.pop("acc_code", None),
|
||||
params.pop("cur_acc_code", None),
|
||||
)
|
||||
return dict()
|
17
ykman-rpc/ykman-rpc.exe.manifest
Normal file
17
ykman-rpc/ykman-rpc.exe.manifest
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows Vista/Server 2008 -->
|
||||
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
|
||||
<!-- Windows 7/Server 2008 R2 -->
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
|
||||
<!-- Windows 8/Server 2012 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
|
||||
<!-- Windows 8.1/Server 2012 R2 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
8
ykman-rpc/ykman-rpc.py
Normal file
8
ykman-rpc/ykman-rpc.py
Normal file
@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from rpc import run_rpc_pipes
|
||||
import sys
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_rpc_pipes(sys.stdout, sys.stdin)
|
52
ykman-rpc/ykman-rpc.spec
Executable file
52
ykman-rpc/ykman-rpc.spec
Executable file
@ -0,0 +1,52 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
block_cipher = None
|
||||
|
||||
|
||||
a = Analysis(
|
||||
["ykman-rpc.py"],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name="ykman-rpc",
|
||||
icon="NONE",
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=True,
|
||||
manifest="ykman-rpc.exe.manifest",
|
||||
version="yubikey-manager/resources/win/version_info.txt",
|
||||
disable_windowed_traceback=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name="ykman-rpc",
|
||||
)
|
1
ykman-rpc/yubikey-manager
Submodule
1
ykman-rpc/yubikey-manager
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit ecd2500c8045c2deb89166cc58362e979639c3a9
|
@ -1 +0,0 @@
|
||||
Subproject commit f90e4d6f59e8acc4399e9ff587f8c821456d069c
|
Loading…
Reference in New Issue
Block a user