remove not needed code for segfault repro
parent
8b9f61507a
commit
438cfe7ba8
@ -1,37 +0,0 @@
|
||||
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.134.0/containers/python-3/.devcontainer/base.Dockerfile
|
||||
|
||||
# [Choice] Python version: 3, 3.10, ...
|
||||
ARG VARIANT=3
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/python:${VARIANT}
|
||||
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# Remove imagemagick due to https://security-tracker.debian.org/tracker/CVE-2019-10131
|
||||
&& apt-get purge -y imagemagick imagemagick-6-common \
|
||||
# Install common packages, non-root user
|
||||
# && /bin/bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" \
|
||||
# Clean up
|
||||
&& apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts
|
||||
|
||||
# [Option] Install Node.js
|
||||
# ARG INSTALL_NODE="false"
|
||||
# ARG NODE_VERSION="lts/*"
|
||||
# RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
|
||||
|
||||
# [Optional] Allow the vscode user to pip install globally w/o sudo
|
||||
# ENV PIP_TARGET=/usr/local/pip-global
|
||||
# ENV PYTHONPATH=${PIP_TARGET}:${PYTHONPATH}
|
||||
# ENV PATH=${PIP_TARGET}/bin:${PATH}
|
||||
# RUN mkdir -p ${PIP_TARGET} \
|
||||
# && chown vscode:root ${PIP_TARGET} \
|
||||
# && echo "if [ \"\$(stat -c '%U' ${PIP_TARGET})\" != \"vscode\" ]; then chown -R vscode:root ${PIP_TARGET}; fi" \
|
||||
# | tee -a /root/.bashrc /home/vscode/.bashrc /root/.zshrc >> /home/vscode/.zshrc
|
||||
|
||||
# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.
|
||||
# COPY requirements.txt /tmp/pip-tmp/
|
||||
# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
|
||||
# && rm -rf /tmp/pip-tmp
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
# RUN apt-get update \
|
||||
# && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
@ -1,22 +0,0 @@
|
||||
// Docu: https://containers.dev/implementors/json_reference/
|
||||
{
|
||||
"name": "Python 3",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
"context": "..",
|
||||
//Update 'VARIANT' to pick a Python version: 3, 3.10, ...
|
||||
"args": {
|
||||
"VARIANT": "3.11"
|
||||
}
|
||||
},
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"ms-python.python"
|
||||
],
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand":
|
||||
"python -m pip install --upgrade pip; pip install -r requirements-dev.txt; pip install -r requirements.txt",
|
||||
"postStartCommand": "echo 'Happy coding'"
|
||||
// Comment out to connect as root instead.
|
||||
// "remoteUser": "vscode"
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
[flake8]
|
||||
ignore =
|
||||
E701
|
||||
exclude =
|
||||
protobuf_generated_python
|
||||
__pycache__
|
@ -1,30 +0,0 @@
|
||||
FROM python:3.11-slim-bullseye
|
||||
|
||||
# https://docs.docker.com/engine/reference/builder/
|
||||
|
||||
# For debugging
|
||||
# docker build . -t extract_otp_secrets --pull --build-arg RUN_TESTS=false
|
||||
# docker run --rm -v "$(pwd)":/files:ro extract_otp_secrets
|
||||
# docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extract_otp_secrets
|
||||
# docker run --entrypoint /bin/bash -it --rm -v "$(pwd)":/files:ro --device="/dev/video0:/dev/video0" --env="DISPLAY" -v /tmp/.X11-unix:/tmp/.X11-unix:ro extract_otp_secrets
|
||||
|
||||
WORKDIR /extract
|
||||
|
||||
COPY . .
|
||||
|
||||
ARG RUN_TESTS=true
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libzbar0 \
|
||||
libsm6 \
|
||||
python3-opencv \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& pip install --no-cache-dir -r requirements.txt \
|
||||
&& if [ "$RUN_TESTS" = "true" ]; then /extract/run_pytest.sh; else echo "Not running tests..."; fi
|
||||
|
||||
WORKDIR /files
|
||||
|
||||
ENTRYPOINT ["python", "/extract/src/extract_otp_secrets.py"]
|
||||
|
||||
LABEL org.opencontainers.image.source https://github.com/scito/extract_otp_secrets
|
||||
LABEL org.opencontainers.image.license GPL-3.0+
|
@ -1,42 +0,0 @@
|
||||
FROM python:3.11-alpine
|
||||
|
||||
# https://docs.docker.com/engine/reference/builder/
|
||||
|
||||
# For debugging
|
||||
# docker run --rm -v "$(pwd)":/files:ro extract_otp_secrets_only_txt
|
||||
# docker build . -t extract_otp_secrets_only_txt -f Dockerfile_only_txt --pull --build-arg RUN_TESTS=false
|
||||
# docker run --entrypoint /bin/sh -it --rm -v "$(pwd)":/files:ro extract_otp_secrets_only_txt
|
||||
# docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extract_otp_secrets_only_txt extract_otp_secrets_test.py -k "not qreader" --relaxed
|
||||
|
||||
WORKDIR /extract
|
||||
|
||||
COPY . .
|
||||
|
||||
ARG RUN_TESTS=true
|
||||
|
||||
RUN apk add --no-cache \
|
||||
zlib \
|
||||
jpeg \
|
||||
&& echo "Arch: $(apk --print-arch)" \
|
||||
&& if [[ "$(apk --print-arch)" == "aarch64" ]]; then apk add --no-cache --virtual .build-deps \
|
||||
gcc \
|
||||
libc-dev \
|
||||
python3-dev \
|
||||
py3-setuptools \
|
||||
zlib-dev \
|
||||
jpeg-dev \
|
||||
; fi \
|
||||
&& pip install --no-cache-dir \
|
||||
protobuf \
|
||||
qrcode \
|
||||
Pillow \
|
||||
colorama \
|
||||
&& if [[ "$(apk --print-arch)" == "aarch64" ]]; then apk del .build-deps; fi \
|
||||
&& if [[ "$RUN_TESTS" == "true" ]]; then /extract/run_pytest.sh tests/extract_otp_secrets_test.py -k "not qreader" --relaxed; else echo "Not running tests..."; fi
|
||||
|
||||
WORKDIR /files
|
||||
|
||||
ENTRYPOINT ["python", "/extract/src/extract_otp_secrets.py"]
|
||||
|
||||
LABEL org.opencontainers.image.source https://github.com/scito/extract_otp_secrets
|
||||
LABEL org.opencontainers.image.license GPL-3.0+
|
@ -1,27 +0,0 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
protobuf = "*"
|
||||
qrcode = "*"
|
||||
pillow = "*"
|
||||
qreader = "*"
|
||||
opencv-contrib-python = "*"
|
||||
colorama = ">=0.4.6"
|
||||
# for macOS: opencv-contrib-python = "<=4.7.0"
|
||||
# for PYTHON <= 3.7: typing_extensions = "*"
|
||||
|
||||
[dev-packages]
|
||||
pytest = "*"
|
||||
pytest-mock = "*"
|
||||
pytest-cov = "*"
|
||||
wheel = "*"
|
||||
flake8 = "*"
|
||||
pylint = "*"
|
||||
mypy = "*"
|
||||
types-protobuf = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.11"
|
@ -1,563 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "25b244c44cb891ac15ef20c4011eb043b87fb1f112396d68f470d0bb362e97f7"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.11"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44",
|
||||
"sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.4.6"
|
||||
},
|
||||
"numpy": {
|
||||
"hashes": [
|
||||
"sha256:0044f7d944ee882400890f9ae955220d29b33d809a038923d88e4e01d652acd9",
|
||||
"sha256:0e3463e6ac25313462e04aea3fb8a0a30fb906d5d300f58b3bc2c23da6a15398",
|
||||
"sha256:179a7ef0889ab769cc03573b6217f54c8bd8e16cef80aad369e1e8185f994cd7",
|
||||
"sha256:2386da9a471cc00a1f47845e27d916d5ec5346ae9696e01a8a34760858fe9dd2",
|
||||
"sha256:26089487086f2648944f17adaa1a97ca6aee57f513ba5f1c0b7ebdabbe2b9954",
|
||||
"sha256:28bc9750ae1f75264ee0f10561709b1462d450a4808cd97c013046073ae64ab6",
|
||||
"sha256:28e418681372520c992805bb723e29d69d6b7aa411065f48216d8329d02ba032",
|
||||
"sha256:442feb5e5bada8408e8fcd43f3360b78683ff12a4444670a7d9e9824c1817d36",
|
||||
"sha256:6ec0c021cd9fe732e5bab6401adea5a409214ca5592cd92a114f7067febcba0c",
|
||||
"sha256:7094891dcf79ccc6bc2a1f30428fa5edb1e6fb955411ffff3401fb4ea93780a8",
|
||||
"sha256:84e789a085aabef2f36c0515f45e459f02f570c4b4c4c108ac1179c34d475ed7",
|
||||
"sha256:87a118968fba001b248aac90e502c0b13606721b1343cdaddbc6e552e8dfb56f",
|
||||
"sha256:8e669fbdcdd1e945691079c2cae335f3e3a56554e06bbd45d7609a6cf568c700",
|
||||
"sha256:ad2925567f43643f51255220424c23d204024ed428afc5aad0f86f3ffc080086",
|
||||
"sha256:b0677a52f5d896e84414761531947c7a330d1adc07c3a4372262f25d84af7bf7",
|
||||
"sha256:b07b40f5fb4fa034120a5796288f24c1fe0e0580bbfff99897ba6267af42def2",
|
||||
"sha256:b09804ff570b907da323b3d762e74432fb07955701b17b08ff1b5ebaa8cfe6a9",
|
||||
"sha256:b162ac10ca38850510caf8ea33f89edcb7b0bb0dfa5592d59909419986b72407",
|
||||
"sha256:b31da69ed0c18be8b77bfce48d234e55d040793cebb25398e2a7d84199fbc7e2",
|
||||
"sha256:caf65a396c0d1f9809596be2e444e3bd4190d86d5c1ce21f5fc4be60a3bc5b36",
|
||||
"sha256:cfa1161c6ac8f92dea03d625c2d0c05e084668f4a06568b77a25a89111621566",
|
||||
"sha256:dae46bed2cb79a58d6496ff6d8da1e3b95ba09afeca2e277628171ca99b99db1",
|
||||
"sha256:ddc7ab52b322eb1e40521eb422c4e0a20716c271a306860979d450decbb51b8e",
|
||||
"sha256:de92efa737875329b052982e37bd4371d52cabf469f83e7b8be9bb7752d67e51",
|
||||
"sha256:e274f0f6c7efd0d577744f52032fdd24344f11c5ae668fe8d01aac0422611df1",
|
||||
"sha256:ed5fb71d79e771ec930566fae9c02626b939e37271ec285e9efaf1b5d4370e7d",
|
||||
"sha256:ef85cf1f693c88c1fd229ccd1055570cb41cdf4875873b7728b6301f12cd05bf",
|
||||
"sha256:f1b739841821968798947d3afcefd386fa56da0caf97722a5de53e07c4ccedc7"
|
||||
],
|
||||
"markers": "python_version >= '3.10'",
|
||||
"version": "==1.24.1"
|
||||
},
|
||||
"opencv-contrib-python": {
|
||||
"hashes": [
|
||||
"sha256:1a48c2f24440cfd6e49c84dbe39c39feff5efbc90be8299c76e7141973d403b6",
|
||||
"sha256:2b8e3a1a7af31ebed28487d161ca4be0edd0b0e241667c6e9c842ac683313b2f",
|
||||
"sha256:2f0c32b0f2f55255632a44bdcfa185f88c7fb6d2616869942aff9d5a39df4997",
|
||||
"sha256:35e9a3809da10a47189c06d4d78b8e7821b9a3578dec8cbddf6ee1675bd83557",
|
||||
"sha256:3a00e12546e5578f6bb7ed408c37fcfea533d74e9691cfaf40926f6b43295577",
|
||||
"sha256:6d1c993811f92ddd7919314ada7b9be1f23db1c73f1384915c834dee8549c0b9",
|
||||
"sha256:7a08f9d1f9dd52de63a7bb448ab7d6d4a1a85b767c2358501d968d1e4d95098d",
|
||||
"sha256:7a75f1775790106e54bcfb101c0e00e1f801a57d9baebc82d0b6758fc83a4ca0",
|
||||
"sha256:86f4b60b9536948f16d2170ba3a9b22d3955a957dc61a9bc56e53692c6db2c7e",
|
||||
"sha256:9829e6efedde1d1b8419c5bd4d62d289ecbf44ae35b843c6da9e3cbcba1a9a8a",
|
||||
"sha256:abc6adfa8694f71a4caffa922b279bd9d96954a37eee40b147f613c64310b411",
|
||||
"sha256:b4033a164b2e2ea0049ba8c1194dab82dca680953ac36f33d1cc2c060906555f",
|
||||
"sha256:e3967b1f3d74b8c70be724dbc07921faec87e8806cc87b2db5e7057815d6a08c",
|
||||
"sha256:e770e9f653a0e5e72b973adb8213fae2df4642730ba1faf31e73a54287a4d5d4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.7.0.68"
|
||||
},
|
||||
"opencv-python": {
|
||||
"hashes": [
|
||||
"sha256:3a00e12546e5578f6bb7ed408c37fcfea533d74e9691cfaf40926f6b43295577",
|
||||
"sha256:6d1c993811f92ddd7919314ada7b9be1f23db1c73f1384915c834dee8549c0b9",
|
||||
"sha256:7a08f9d1f9dd52de63a7bb448ab7d6d4a1a85b767c2358501d968d1e4d95098d",
|
||||
"sha256:86f4b60b9536948f16d2170ba3a9b22d3955a957dc61a9bc56e53692c6db2c7e",
|
||||
"sha256:9829e6efedde1d1b8419c5bd4d62d289ecbf44ae35b843c6da9e3cbcba1a9a8a",
|
||||
"sha256:abc6adfa8694f71a4caffa922b279bd9d96954a37eee40b147f613c64310b411",
|
||||
"sha256:e770e9f653a0e5e72b973adb8213fae2df4642730ba1faf31e73a54287a4d5d4"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==4.7.0.68"
|
||||
},
|
||||
"pillow": {
|
||||
"hashes": [
|
||||
"sha256:03150abd92771742d4a8cd6f2fa6246d847dcd2e332a18d0c15cc75bf6703040",
|
||||
"sha256:073adb2ae23431d3b9bcbcff3fe698b62ed47211d0716b067385538a1b0f28b8",
|
||||
"sha256:0b07fffc13f474264c336298d1b4ce01d9c5a011415b79d4ee5527bb69ae6f65",
|
||||
"sha256:0b7257127d646ff8676ec8a15520013a698d1fdc48bc2a79ba4e53df792526f2",
|
||||
"sha256:12ce4932caf2ddf3e41d17fc9c02d67126935a44b86df6a206cf0d7161548627",
|
||||
"sha256:15c42fb9dea42465dfd902fb0ecf584b8848ceb28b41ee2b58f866411be33f07",
|
||||
"sha256:18498994b29e1cf86d505edcb7edbe814d133d2232d256db8c7a8ceb34d18cef",
|
||||
"sha256:1c7c8ae3864846fc95f4611c78129301e203aaa2af813b703c55d10cc1628535",
|
||||
"sha256:22b012ea2d065fd163ca096f4e37e47cd8b59cf4b0fd47bfca6abb93df70b34c",
|
||||
"sha256:276a5ca930c913f714e372b2591a22c4bd3b81a418c0f6635ba832daec1cbcfc",
|
||||
"sha256:2e0918e03aa0c72ea56edbb00d4d664294815aa11291a11504a377ea018330d3",
|
||||
"sha256:3033fbe1feb1b59394615a1cafaee85e49d01b51d54de0cbf6aa8e64182518a1",
|
||||
"sha256:3168434d303babf495d4ba58fc22d6604f6e2afb97adc6a423e917dab828939c",
|
||||
"sha256:32a44128c4bdca7f31de5be641187367fe2a450ad83b833ef78910397db491aa",
|
||||
"sha256:3dd6caf940756101205dffc5367babf288a30043d35f80936f9bfb37f8355b32",
|
||||
"sha256:40e1ce476a7804b0fb74bcfa80b0a2206ea6a882938eaba917f7a0f004b42502",
|
||||
"sha256:41e0051336807468be450d52b8edd12ac60bebaa97fe10c8b660f116e50b30e4",
|
||||
"sha256:4390e9ce199fc1951fcfa65795f239a8a4944117b5935a9317fb320e7767b40f",
|
||||
"sha256:502526a2cbfa431d9fc2a079bdd9061a2397b842bb6bc4239bb176da00993812",
|
||||
"sha256:51e0e543a33ed92db9f5ef69a0356e0b1a7a6b6a71b80df99f1d181ae5875636",
|
||||
"sha256:57751894f6618fd4308ed8e0c36c333e2f5469744c34729a27532b3db106ee20",
|
||||
"sha256:5d77adcd56a42d00cc1be30843d3426aa4e660cab4a61021dc84467123f7a00c",
|
||||
"sha256:655a83b0058ba47c7c52e4e2df5ecf484c1b0b0349805896dd350cbc416bdd91",
|
||||
"sha256:68943d632f1f9e3dce98908e873b3a090f6cba1cbb1b892a9e8d97c938871fbe",
|
||||
"sha256:6c738585d7a9961d8c2821a1eb3dcb978d14e238be3d70f0a706f7fa9316946b",
|
||||
"sha256:73bd195e43f3fadecfc50c682f5055ec32ee2c933243cafbfdec69ab1aa87cad",
|
||||
"sha256:772a91fc0e03eaf922c63badeca75e91baa80fe2f5f87bdaed4280662aad25c9",
|
||||
"sha256:77ec3e7be99629898c9a6d24a09de089fa5356ee408cdffffe62d67bb75fdd72",
|
||||
"sha256:7db8b751ad307d7cf238f02101e8e36a128a6cb199326e867d1398067381bff4",
|
||||
"sha256:801ec82e4188e935c7f5e22e006d01611d6b41661bba9fe45b60e7ac1a8f84de",
|
||||
"sha256:82409ffe29d70fd733ff3c1025a602abb3e67405d41b9403b00b01debc4c9a29",
|
||||
"sha256:828989c45c245518065a110434246c44a56a8b2b2f6347d1409c787e6e4651ee",
|
||||
"sha256:829f97c8e258593b9daa80638aee3789b7df9da5cf1336035016d76f03b8860c",
|
||||
"sha256:871b72c3643e516db4ecf20efe735deb27fe30ca17800e661d769faab45a18d7",
|
||||
"sha256:89dca0ce00a2b49024df6325925555d406b14aa3efc2f752dbb5940c52c56b11",
|
||||
"sha256:90fb88843d3902fe7c9586d439d1e8c05258f41da473952aa8b328d8b907498c",
|
||||
"sha256:97aabc5c50312afa5e0a2b07c17d4ac5e865b250986f8afe2b02d772567a380c",
|
||||
"sha256:9aaa107275d8527e9d6e7670b64aabaaa36e5b6bd71a1015ddd21da0d4e06448",
|
||||
"sha256:9f47eabcd2ded7698106b05c2c338672d16a6f2a485e74481f524e2a23c2794b",
|
||||
"sha256:a0a06a052c5f37b4ed81c613a455a81f9a3a69429b4fd7bb913c3fa98abefc20",
|
||||
"sha256:ab388aaa3f6ce52ac1cb8e122c4bd46657c15905904b3120a6248b5b8b0bc228",
|
||||
"sha256:ad58d27a5b0262c0c19b47d54c5802db9b34d38bbf886665b626aff83c74bacd",
|
||||
"sha256:ae5331c23ce118c53b172fa64a4c037eb83c9165aba3a7ba9ddd3ec9fa64a699",
|
||||
"sha256:af0372acb5d3598f36ec0914deed2a63f6bcdb7b606da04dc19a88d31bf0c05b",
|
||||
"sha256:afa4107d1b306cdf8953edde0534562607fe8811b6c4d9a486298ad31de733b2",
|
||||
"sha256:b03ae6f1a1878233ac620c98f3459f79fd77c7e3c2b20d460284e1fb370557d4",
|
||||
"sha256:b0915e734b33a474d76c28e07292f196cdf2a590a0d25bcc06e64e545f2d146c",
|
||||
"sha256:b4012d06c846dc2b80651b120e2cdd787b013deb39c09f407727ba90015c684f",
|
||||
"sha256:b472b5ea442148d1c3e2209f20f1e0bb0eb556538690fa70b5e1f79fa0ba8dc2",
|
||||
"sha256:b59430236b8e58840a0dfb4099a0e8717ffb779c952426a69ae435ca1f57210c",
|
||||
"sha256:b90f7616ea170e92820775ed47e136208e04c967271c9ef615b6fbd08d9af0e3",
|
||||
"sha256:b9a65733d103311331875c1dca05cb4606997fd33d6acfed695b1232ba1df193",
|
||||
"sha256:bac18ab8d2d1e6b4ce25e3424f709aceef668347db8637c2296bcf41acb7cf48",
|
||||
"sha256:bca31dd6014cb8b0b2db1e46081b0ca7d936f856da3b39744aef499db5d84d02",
|
||||
"sha256:be55f8457cd1eac957af0c3f5ece7bc3f033f89b114ef30f710882717670b2a8",
|
||||
"sha256:c7025dce65566eb6e89f56c9509d4f628fddcedb131d9465cacd3d8bac337e7e",
|
||||
"sha256:c935a22a557a560108d780f9a0fc426dd7459940dc54faa49d83249c8d3e760f",
|
||||
"sha256:dbb8e7f2abee51cef77673be97760abff1674ed32847ce04b4af90f610144c7b",
|
||||
"sha256:e6ea6b856a74d560d9326c0f5895ef8050126acfdc7ca08ad703eb0081e82b74",
|
||||
"sha256:ebf2029c1f464c59b8bdbe5143c79fa2045a581ac53679733d3a91d400ff9efb",
|
||||
"sha256:f1ff2ee69f10f13a9596480335f406dd1f70c3650349e2be67ca3139280cade0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==9.3.0"
|
||||
},
|
||||
"protobuf": {
|
||||
"hashes": [
|
||||
"sha256:1f22ac0ca65bb70a876060d96d914dae09ac98d114294f77584b0d2644fa9c30",
|
||||
"sha256:237216c3326d46808a9f7c26fd1bd4b20015fb6867dc5d263a493ef9a539293b",
|
||||
"sha256:27f4d15021da6d2b706ddc3860fac0a5ddaba34ab679dc182b60a8bb4e1121cc",
|
||||
"sha256:299ea899484ee6f44604deb71f424234f654606b983cb496ea2a53e3c63ab791",
|
||||
"sha256:3d164928ff0727d97022957c2b849250ca0e64777ee31efd7d6de2e07c494717",
|
||||
"sha256:6ab80df09e3208f742c98443b6166bcb70d65f52cfeb67357d52032ea1ae9bec",
|
||||
"sha256:78a28c9fa223998472886c77042e9b9afb6fe4242bd2a2a5aced88e3f4422aa7",
|
||||
"sha256:7cd532c4566d0e6feafecc1059d04c7915aec8e182d1cf7adee8b24ef1e2e6ab",
|
||||
"sha256:89f9149e4a0169cddfc44c74f230d7743002e3aa0b9472d8c28f0388102fc4c2",
|
||||
"sha256:a53fd3f03e578553623272dc46ac2f189de23862e68565e83dde203d41b76fc5",
|
||||
"sha256:b135410244ebe777db80298297a97fbb4c862c881b4403b71bac9d4107d61fd1",
|
||||
"sha256:b98d0148f84e3a3c569e19f52103ca1feacdac0d2df8d6533cf983d1fda28462",
|
||||
"sha256:d1736130bce8cf131ac7957fa26880ca19227d4ad68b4888b3be0dea1f95df97",
|
||||
"sha256:f45460f9ee70a0ec1b6694c6e4e348ad2019275680bd68a1d9314b8c7e01e574"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.21.12"
|
||||
},
|
||||
"pyzbar": {
|
||||
"hashes": [
|
||||
"sha256:13e3ee5a2f3a545204a285f41814d5c0db571967e8d4af8699a03afc55182a9c",
|
||||
"sha256:4559628b8192feb25766d954b36a3753baaf5c97c03135aec7e4a026036b475d",
|
||||
"sha256:8f4c5264c9c7c6b9f20d01efc52a4eba1ded47d9ba857a94130afe33703eb518"
|
||||
],
|
||||
"version": "==0.1.9"
|
||||
},
|
||||
"qrcode": {
|
||||
"hashes": [
|
||||
"sha256:375a6ff240ca9bd41adc070428b5dfc1dcfbb0f2507f1ac848f6cded38956578"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.3.1"
|
||||
},
|
||||
"qreader": {
|
||||
"hashes": [
|
||||
"sha256:f96d2879c9f47f9641da55dc3e1df765cd4d6f1cfbc8b7fa811b70c1e27d4fba"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.2"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
"sha256:10e0ad5f7b79c435179d0d0f0df69998c4eef4597534aae44910db060baeb907",
|
||||
"sha256:1493fe8bd3dfd73dc35bd53c9d5b6e49ead98497c47b2307662556a5692d29d7"
|
||||
],
|
||||
"markers": "python_full_version >= '3.7.2'",
|
||||
"version": "==2.12.13"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836",
|
||||
"sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==22.2.0"
|
||||
},
|
||||
"coverage": {
|
||||
"extras": [
|
||||
"toml"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:07bcfb1d8ac94af886b54e18a88b393f6a73d5959bb31e46644a02453c36e475",
|
||||
"sha256:09f6b5a8415b6b3e136d5fec62b552972187265cb705097bf030eb9d4ffb9b60",
|
||||
"sha256:0a79137fc99815fff6a852c233628e735ec15903cfd16da0f229d9c4d45926ab",
|
||||
"sha256:0b4b3a4d9915b2be879aff6299c0a6129f3d08a775d5a061f503cf79571f73e4",
|
||||
"sha256:1285648428a6101b5f41a18991c84f1c3959cee359e51b8375c5882fc364a13f",
|
||||
"sha256:12a5aa77783d49e05439fbe6e6b427484f8a0f9f456b46a51d8aac022cfd024d",
|
||||
"sha256:19ec666533f0f70a0993f88b8273057b96c07b9d26457b41863ccd021a043b9a",
|
||||
"sha256:1e414dc32ee5c3f36544ea466b6f52f28a7af788653744b8570d0bf12ff34bc0",
|
||||
"sha256:2c44fcfb3781b41409d0f060a4ed748537557de9362a8a9282182fafb7a76ab4",
|
||||
"sha256:397b4a923cc7566bbc7ae2dfd0ba5a039b61d19c740f1373791f2ebd11caea59",
|
||||
"sha256:3cfc595d2af13856505631be072835c59f1acf30028d1c860b435c5fc9c15b69",
|
||||
"sha256:3dd4ee135e08037f458425b8842d24a95a0961831a33f89685ff86b77d378f89",
|
||||
"sha256:486ee81fa694b4b796fc5617e376326a088f7b9729c74d9defa211813f3861e4",
|
||||
"sha256:4f943a3b2bc520102dd3e0bb465e1286e12c9a54f58accd71b9e65324d9c7c01",
|
||||
"sha256:63d56165a7c76265468d7e0c5548215a5ba515fc2cba5232d17df97bffa10f6c",
|
||||
"sha256:66b18c3cf8bbab0cce0d7b9e4262dc830e93588986865a8c78ab2ae324b3ed56",
|
||||
"sha256:691571f31ace1837838b7e421d3a09a8c00b4aac32efacb4fc9bd0a5c647d25a",
|
||||
"sha256:6c5ad996c6fa4d8ed669cfa1e8551348729d008a2caf81489ab9ea67cfbc7498",
|
||||
"sha256:6d55d840e1b8c0002fce66443e124e8581f30f9ead2e54fbf6709fb593181f2c",
|
||||
"sha256:72d1507f152abacea81f65fee38e4ef3ac3c02ff8bc16f21d935fd3a8a4ad910",
|
||||
"sha256:74f70cd92669394eaf8d7756d1b195c8032cf7bbbdfce3bc489d4e15b3b8cf73",
|
||||
"sha256:830525361249dc4cd013652b0efad645a385707a5ae49350c894b67d23fbb07c",
|
||||
"sha256:854f22fa361d1ff914c7efa347398374cc7d567bdafa48ac3aa22334650dfba2",
|
||||
"sha256:89caf4425fe88889e2973a8e9a3f6f5f9bbe5dd411d7d521e86428c08a873a4a",
|
||||
"sha256:9158f8fb06747ac17bd237930c4372336edc85b6e13bdc778e60f9d685c3ca37",
|
||||
"sha256:92651580bd46519067e36493acb394ea0607b55b45bd81dd4e26379ed1871f55",
|
||||
"sha256:978258fec36c154b5e250d356c59af7d4c3ba02bef4b99cda90b6029441d797d",
|
||||
"sha256:9823e4789ab70f3ec88724bba1a203f2856331986cd893dedbe3e23a6cfc1e4e",
|
||||
"sha256:9b373c9345c584bb4b5f5b8840df7f4ab48c4cbb7934b58d52c57020d911b856",
|
||||
"sha256:a4a574a19eeb67575a5328a5760bbbb737faa685616586a9f9da4281f940109c",
|
||||
"sha256:aec2d1515d9d39ff270059fd3afbb3b44e6ec5758af73caf18991807138c7118",
|
||||
"sha256:b3695c4f4750bca943b3e1f74ad4be8d29e4aeab927d50772c41359107bd5d5c",
|
||||
"sha256:b3763e7fcade2ff6c8e62340af9277f54336920489ceb6a8cd6cc96da52fcc62",
|
||||
"sha256:b66bb21a23680dee0be66557dc6b02a3152ddb55edf9f6723fa4a93368f7158d",
|
||||
"sha256:b6f22bb64cc39bcb883e5910f99a27b200fdc14cdd79df8696fa96b0005c9444",
|
||||
"sha256:b77015d1cb8fe941be1222a5a8b4e3fbca88180cfa7e2d4a4e58aeabadef0ab7",
|
||||
"sha256:b9ea158775c7c2d3e54530a92da79496fb3fb577c876eec761c23e028f1e216c",
|
||||
"sha256:c20cfebcc149a4c212f6491a5f9ff56f41829cd4f607b5be71bb2d530ef243b1",
|
||||
"sha256:cfded268092a84605f1cc19e5c737f9ce630a8900a3589e9289622db161967e9",
|
||||
"sha256:d1991f1dd95eba69d2cd7708ff6c2bbd2426160ffc73c2b81f617a053ebcb1a8",
|
||||
"sha256:d3022c3007d3267a880b5adcf18c2a9bf1fc64469b394a804886b401959b8742",
|
||||
"sha256:d6814854c02cbcd9c873c0f3286a02e3ac1250625cca822ca6bc1018c5b19f1c",
|
||||
"sha256:d87717959d4d0ee9db08a0f1d80d21eb585aafe30f9b0a54ecf779a69cb015f6",
|
||||
"sha256:e00c14720b8b3b6c23b487e70bd406abafc976ddc50490f645166f111c419c39",
|
||||
"sha256:e60bef2e2416f15fdc05772bf87db06c6a6f9870d1db08fdd019fbec98ae24a9",
|
||||
"sha256:e78e9dcbf4f3853d3ae18a8f9272111242531535ec9e1009fa8ec4a2b74557dc",
|
||||
"sha256:f66460f17c9319ea4f91c165d46840314f0a7c004720b20be58594d162a441d8",
|
||||
"sha256:fa6a5a224b7f4cfb226f4fc55a57e8537fcc096f42219128c2c74c0e7d0953e1",
|
||||
"sha256:fb992c47cb1e5bd6a01e97182400bcc2ba2077080a17fcd7be23aaa6e572e390",
|
||||
"sha256:fd1b9c5adc066db699ccf7fa839189a649afcdd9e02cb5dc9d24e67e7922737d",
|
||||
"sha256:fd556ff16a57a070ce4f31c635953cc44e25244f91a0378c6e9bdfd40fdb249f"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==7.0.1"
|
||||
},
|
||||
"dill": {
|
||||
"hashes": [
|
||||
"sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0",
|
||||
"sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"
|
||||
],
|
||||
"markers": "python_version >= '3.11'",
|
||||
"version": "==0.3.6"
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7",
|
||||
"sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==6.0.0"
|
||||
},
|
||||
"iniconfig": {
|
||||
"hashes": [
|
||||
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
|
||||
"sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"isort": {
|
||||
"hashes": [
|
||||
"sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6",
|
||||
"sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"
|
||||
],
|
||||
"markers": "python_full_version >= '3.7.0'",
|
||||
"version": "==5.11.4"
|
||||
},
|
||||
"lazy-object-proxy": {
|
||||
"hashes": [
|
||||
"sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada",
|
||||
"sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d",
|
||||
"sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7",
|
||||
"sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe",
|
||||
"sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd",
|
||||
"sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c",
|
||||
"sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858",
|
||||
"sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288",
|
||||
"sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec",
|
||||
"sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f",
|
||||
"sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891",
|
||||
"sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c",
|
||||
"sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25",
|
||||
"sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156",
|
||||
"sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8",
|
||||
"sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f",
|
||||
"sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e",
|
||||
"sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0",
|
||||
"sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.8.0"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
"sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
|
||||
"sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.7.0"
|
||||
},
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
"sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d",
|
||||
"sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6",
|
||||
"sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf",
|
||||
"sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f",
|
||||
"sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813",
|
||||
"sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33",
|
||||
"sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad",
|
||||
"sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05",
|
||||
"sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297",
|
||||
"sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06",
|
||||
"sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd",
|
||||
"sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243",
|
||||
"sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305",
|
||||
"sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476",
|
||||
"sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711",
|
||||
"sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70",
|
||||
"sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5",
|
||||
"sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461",
|
||||
"sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab",
|
||||
"sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c",
|
||||
"sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d",
|
||||
"sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135",
|
||||
"sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93",
|
||||
"sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648",
|
||||
"sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a",
|
||||
"sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb",
|
||||
"sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3",
|
||||
"sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372",
|
||||
"sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb",
|
||||
"sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.991"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
|
||||
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
|
||||
],
|
||||
"version": "==0.4.3"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3",
|
||||
"sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==22.0"
|
||||
},
|
||||
"platformdirs": {
|
||||
"hashes": [
|
||||
"sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490",
|
||||
"sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.6.2"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
|
||||
"sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053",
|
||||
"sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.10.0"
|
||||
},
|
||||
"pyflakes": {
|
||||
"hashes": [
|
||||
"sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf",
|
||||
"sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"pylint": {
|
||||
"hashes": [
|
||||
"sha256:18783cca3cfee5b83c6c5d10b3cdb66c6594520ffae61890858fe8d932e1c6b4",
|
||||
"sha256:349c8cd36aede4d50a0754a8c0218b43323d13d5d88f4b2952ddfe3e169681eb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.15.9"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71",
|
||||
"sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.2.0"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
"sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b",
|
||||
"sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"pytest-mock": {
|
||||
"hashes": [
|
||||
"sha256:f4c973eeae0282963eb293eb173ce91b091a79c1334455acfac9ddee8a1c784b",
|
||||
"sha256:fbbdb085ef7c252a326fd8cdcac0aa3b1333d8811f131bdcc701002e1be7ed4f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.10.0"
|
||||
},
|
||||
"tomlkit": {
|
||||
"hashes": [
|
||||
"sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b",
|
||||
"sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.11.6"
|
||||
},
|
||||
"types-protobuf": {
|
||||
"hashes": [
|
||||
"sha256:7df483d34ad3fcb1fa7fff1073560d596c9ac1f419cfa851b220c9a93386c998",
|
||||
"sha256:aeefcf39d637016998b3c7b699750847071b555f7c2e0c9873d42ab6103d1a39"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.21.0.2"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa",
|
||||
"sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==4.4.0"
|
||||
},
|
||||
"wheel": {
|
||||
"hashes": [
|
||||
"sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac",
|
||||
"sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.38.4"
|
||||
},
|
||||
"wrapt": {
|
||||
"hashes": [
|
||||
"sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3",
|
||||
"sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b",
|
||||
"sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4",
|
||||
"sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2",
|
||||
"sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656",
|
||||
"sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3",
|
||||
"sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff",
|
||||
"sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310",
|
||||
"sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a",
|
||||
"sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57",
|
||||
"sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069",
|
||||
"sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383",
|
||||
"sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe",
|
||||
"sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87",
|
||||
"sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d",
|
||||
"sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b",
|
||||
"sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907",
|
||||
"sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f",
|
||||
"sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0",
|
||||
"sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28",
|
||||
"sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1",
|
||||
"sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853",
|
||||
"sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc",
|
||||
"sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3",
|
||||
"sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3",
|
||||
"sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164",
|
||||
"sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1",
|
||||
"sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c",
|
||||
"sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1",
|
||||
"sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7",
|
||||
"sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1",
|
||||
"sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320",
|
||||
"sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed",
|
||||
"sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1",
|
||||
"sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248",
|
||||
"sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c",
|
||||
"sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456",
|
||||
"sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77",
|
||||
"sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef",
|
||||
"sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1",
|
||||
"sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7",
|
||||
"sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86",
|
||||
"sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4",
|
||||
"sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d",
|
||||
"sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d",
|
||||
"sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8",
|
||||
"sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5",
|
||||
"sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471",
|
||||
"sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00",
|
||||
"sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68",
|
||||
"sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3",
|
||||
"sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d",
|
||||
"sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735",
|
||||
"sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d",
|
||||
"sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569",
|
||||
"sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7",
|
||||
"sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59",
|
||||
"sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5",
|
||||
"sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb",
|
||||
"sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b",
|
||||
"sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f",
|
||||
"sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462",
|
||||
"sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015",
|
||||
"sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"
|
||||
],
|
||||
"markers": "python_version >= '3.11'",
|
||||
"version": "==1.14.1"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"packages": [
|
||||
"python310",
|
||||
"python310Packages.protobuf",
|
||||
"python310Packages.qrcode"
|
||||
]
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 653 KiB |
@ -1,19 +0,0 @@
|
||||
# 2FA example from https://www.raspberrypi.org/blog/setting-up-two-factor-authentication-on-your-raspberry-pi/
|
||||
|
||||
# Secret key: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
otpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B
|
||||
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4%2F%2F%2F%2F%2FwE%3D
|
||||
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B
|
||||
|
||||
# otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||
otpauth-migration://offline?data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D
|
||||
|
||||
# otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
# Name: "encoding: ¿äÄéÉ? (demo)"
|
||||
otpauth-migration://offline?data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D
|
@ -1,2 +0,0 @@
|
||||
Title,User Name,HmacOtp-Secret-Base32,HmacOtp-Counter,Group
|
||||
,hotp demo,7KSQL2JTUDIS5EF65KLMRQIIGY,4,OTP/HOTP
|
|
@ -1,6 +0,0 @@
|
||||
Title,User Name,TimeOtp-Secret-Base32,Group
|
||||
raspberrypi,pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
|
||||
,pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
|
||||
,pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
|
||||
raspberrypi,pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
|
||||
,encoding: ¿äÄéÉ? (demo),7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
|
|
@ -1,7 +0,0 @@
|
||||
name,secret,issuer,type,counter,url
|
||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,raspberrypi,totp,,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,,totp,,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,,totp,,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,raspberrypi,totp,,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
hotp demo,7KSQL2JTUDIS5EF65KLMRQIIGY,,hotp,4,otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||
encoding: ¿äÄéÉ? (demo),7KSQL2JTUDIS5EF65KLMRQIIGY,,totp,,otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
|
@ -1,50 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "pi@raspberrypi",
|
||||
"secret": "7KSQL2JTUDIS5EF65KLMRQIIGY",
|
||||
"issuer": "raspberrypi",
|
||||
"type": "totp",
|
||||
"counter": null,
|
||||
"url": "otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi"
|
||||
},
|
||||
{
|
||||
"name": "pi@raspberrypi",
|
||||
"secret": "7KSQL2JTUDIS5EF65KLMRQIIGY",
|
||||
"issuer": "",
|
||||
"type": "totp",
|
||||
"counter": null,
|
||||
"url": "otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY"
|
||||
},
|
||||
{
|
||||
"name": "pi@raspberrypi",
|
||||
"secret": "7KSQL2JTUDIS5EF65KLMRQIIGY",
|
||||
"issuer": "",
|
||||
"type": "totp",
|
||||
"counter": null,
|
||||
"url": "otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY"
|
||||
},
|
||||
{
|
||||
"name": "pi@raspberrypi",
|
||||
"secret": "7KSQL2JTUDIS5EF65KLMRQIIGY",
|
||||
"issuer": "raspberrypi",
|
||||
"type": "totp",
|
||||
"counter": null,
|
||||
"url": "otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi"
|
||||
},
|
||||
{
|
||||
"name": "hotp demo",
|
||||
"secret": "7KSQL2JTUDIS5EF65KLMRQIIGY",
|
||||
"issuer": "",
|
||||
"type": "hotp",
|
||||
"counter": 4,
|
||||
"url": "otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4"
|
||||
},
|
||||
{
|
||||
"name": "encoding: ¿äÄéÉ? (demo)",
|
||||
"secret": "7KSQL2JTUDIS5EF65KLMRQIIGY",
|
||||
"issuer": "",
|
||||
"type": "totp",
|
||||
"counter": null,
|
||||
"url": "otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY"
|
||||
}
|
||||
]
|
@ -1,4 +0,0 @@
|
||||
[mypy]
|
||||
|
||||
[mypy-protobuf_generated_python.*]
|
||||
ignore_errors = True
|
@ -1,66 +0,0 @@
|
||||
[build-system]
|
||||
requires = [
|
||||
"setuptools>=64.0.0", "wheel>=0.37.0", "pip",
|
||||
# https://setuptools-git-versioning.readthedocs.io/en/latest/differences.html
|
||||
"setuptools-git-versioning",
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "extract_otp_secrets"
|
||||
# https://pypi.org/classifiers/
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Console",
|
||||
"Environment :: X11 Applications :: Qt",
|
||||
"Environment :: Win32 (MS Windows)",
|
||||
"Topic :: System :: Archiving :: Backup",
|
||||
"Topic :: Utilities",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: System Administrators",
|
||||
"Programming Language :: Python",
|
||||
"Natural Language :: English",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
]
|
||||
dependencies = [
|
||||
"protobuf",
|
||||
"qrcode",
|
||||
"Pillow",
|
||||
"qreader",
|
||||
"pyzbar",
|
||||
"opencv-contrib-python>=4.7.0; sys_platform == 'darwin'",
|
||||
"opencv-contrib-python; sys_platform != 'darwin'",
|
||||
"typing_extensions; python_version<='3.7'",
|
||||
"colorama>=0.4.6",
|
||||
]
|
||||
description = "Extract two-factor authentication (2FA, TFA, OTP) secret keys from export QR codes of 'Google Authenticator' app"
|
||||
dynamic = ["version"]
|
||||
keywords = ["python", "security", "json", "otp", "csv", "protobuf", "qrcode", "two-factor", "totp", "google-authenticator", "recovery", "proto3", "mfa", "two-factor-authentication", "tfa", "qr-codes", "otpauth", "2fa", "security-tools"]
|
||||
license = {text = "GNU General Public License v3 (GPLv3)"}
|
||||
readme = "README.md"
|
||||
authors = [{name = "scito", email = "info@scito.ch"}]
|
||||
maintainers = [{name = "scito", email = "info@scito.ch"}]
|
||||
requires-python = ">=3.7, <4"
|
||||
scripts = {extract_otp_secrets = "extract_otp_secrets:sys_main"}
|
||||
urls = {Project-URL = "https://github.com/scito/extract_otp_secret_keys", Bug-Reports = "https://github.com/scito/extract_otp_secret_keys/issues", Source = "https://github.com/scito/extract_otp_secret_keys" }
|
||||
|
||||
# [tool.setuptools]
|
||||
# Still in beta, once it is stable move config from setup.cfg to pyproject.toml
|
||||
# py-modules = ["extract_otp_secrets", "protobuf_generated_python.protobuf_generated_python"]
|
||||
|
||||
# [tool.setuptools.dynamic]
|
||||
# version = {attr = "extract_otp_secrets.VERSION"}
|
||||
|
||||
[tool.setuptools-git-versioning]
|
||||
enabled = true
|
||||
|
||||
# https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure%3E
|
||||
# https://docs.pytest.org/en/7.1.x/explanation/goodpractices.html#which-import-mode
|
||||
[tool.pytest.ini_options]
|
||||
addopts = [ "--import-mode=importlib", ]
|
@ -1,3 +0,0 @@
|
||||
[pytest]
|
||||
markers =
|
||||
qreader: QR image reader tests
|
@ -1,10 +0,0 @@
|
||||
flake8
|
||||
mypy
|
||||
types-protobuf
|
||||
pylint
|
||||
pytest
|
||||
pytest-mock
|
||||
pytest-cov
|
||||
setuptools
|
||||
wheel
|
||||
build
|
@ -1,9 +0,0 @@
|
||||
protobuf
|
||||
qrcode
|
||||
Pillow
|
||||
qreader
|
||||
opencv-contrib-python>=4.7.0; sys_platform == 'darwin'
|
||||
opencv-contrib-python; sys_platform != 'darwin'
|
||||
pyzbar
|
||||
typing_extensions; python_version<='3.7'
|
||||
colorama>=0.4.6
|
@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
cd /extract
|
||||
pip install -U pytest pytest-mock && pip install --no-deps . && pytest "$@"
|
@ -1,12 +0,0 @@
|
||||
[metadata]
|
||||
name = extract_otp_secrets
|
||||
|
||||
[options]
|
||||
python_requires = >=3.7, <4
|
||||
py_modules = extract_otp_secrets, protobuf_generated_python.google_auth_pb2
|
||||
package_dir =
|
||||
=src
|
||||
# packages=find:
|
||||
|
||||
# [options.packages.find]
|
||||
# where=src
|
@ -1,666 +0,0 @@
|
||||
# Extract two-factor authentication (2FA, TFA) secret keys from export QR codes of "Google Authenticator" app
|
||||
#
|
||||
# Usage:
|
||||
# 1. Export the QR codes from "Google Authenticator" app
|
||||
# 2. Read QR codes with QR code reader (e.g. with a second device)
|
||||
# 3. Save the captured QR codes in a text file. Save each QR code on a new line. (The captured QR codes look like "otpauth-migration://offline?data=...")
|
||||
# 4. Call this script with the file as input:
|
||||
# python extract_otp_secrets.py example_export.txt
|
||||
#
|
||||
# Requirement:
|
||||
# The protobuf package of Google for proto3 is required for running this script.
|
||||
# pip install protobuf
|
||||
#
|
||||
# Optional:
|
||||
# For printing QR codes, the qrcode module is required
|
||||
# pip install qrcode
|
||||
#
|
||||
# Technical background:
|
||||
# The export QR code of "Google Authenticator" contains the URL "otpauth-migration://offline?data=...".
|
||||
# The data parameter is a base64 encoded proto3 message (Google Protocol Buffers).
|
||||
#
|
||||
# Command for regeneration of Python code from proto3 message definition file (only necessary in case of changes of the proto3 message definition):
|
||||
# protoc --python_out=generated_python google_auth.proto
|
||||
#
|
||||
# References:
|
||||
# Proto3 documentation: https://developers.google.com/protocol-buffers/docs/pythontutorial
|
||||
# Template code: https://github.com/beemdevelopment/Aegis/pull/406
|
||||
|
||||
# Author: Scito (https://scito.ch)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations # for compatibility with PYTHON < 3.11
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import csv
|
||||
import fileinput
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import urllib.parse as urlparse
|
||||
from enum import Enum
|
||||
from operator import add
|
||||
from typing import Any, List, Optional, TextIO, Tuple, Union
|
||||
|
||||
# PYTHON < 3.8: compatibility
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import Final, TypedDict
|
||||
else:
|
||||
from typing_extensions import Final, TypedDict
|
||||
|
||||
from qrcode import QRCode # type: ignore
|
||||
|
||||
import protobuf_generated_python.google_auth_pb2 as pb
|
||||
import colorama
|
||||
|
||||
try:
|
||||
import cv2 # type: ignore
|
||||
import numpy
|
||||
|
||||
try:
|
||||
import pyzbar.pyzbar as zbar # type: ignore
|
||||
from qreader import QReader # type: ignore
|
||||
except ImportError as e:
|
||||
raise SystemExit(f"""
|
||||
ERROR: Cannot import QReader module. This problem is probably due to the missing zbar shared library.
|
||||
On Linux and macOS libzbar0 must be installed.
|
||||
See in README.md for the installation of the libzbar0.
|
||||
Exception: {e}""")
|
||||
|
||||
# Types
|
||||
# PYTHON > 3.9: Final[tuple[int]]
|
||||
ColorBGR = Tuple[int, int, int] # RGB Color specified as Blue, Green, Red
|
||||
Point = Tuple[int, int]
|
||||
|
||||
# CV2 camera capture constants
|
||||
FONT: Final[int] = cv2.FONT_HERSHEY_PLAIN
|
||||
FONT_SCALE: Final[int] = 1
|
||||
FONT_THICKNESS: Final[int] = 1
|
||||
FONT_LINE_STYLE: Final[int] = cv2.LINE_AA
|
||||
RECT_THICKNESS: Final[int] = 5
|
||||
# PYTHON <= 3.7: must use () for assignments
|
||||
START_POS_TEXT: Final[Point] = (5, 20)
|
||||
NORMAL_COLOR: Final[ColorBGR] = (255, 0, 255)
|
||||
SUCCESS_COLOR: Final[ColorBGR] = (0, 255, 0)
|
||||
FAILURE_COLOR: Final[ColorBGR] = (0, 0, 255)
|
||||
FONT_DY: Final[Tuple[int, int]] = (0, cv2.getTextSize("M", FONT, FONT_SCALE, FONT_THICKNESS)[0][1] + 5)
|
||||
|
||||
qreader_available = True
|
||||
except ImportError:
|
||||
qreader_available = False
|
||||
|
||||
# Workaround for PYTHON < 3.10: Union[int, None] used instead of int | None
|
||||
|
||||
# Types
|
||||
Args = argparse.Namespace
|
||||
OtpUrl = str
|
||||
# PYTHON > 3.7: Otp = TypedDict('Otp', {'name': str, 'secret': str, 'issuer': str, 'type': str, 'counter': int | None, 'url': OtpUrl})
|
||||
Otp = TypedDict('Otp', {'name': str, 'secret': str, 'issuer': str, 'type': str, 'counter': Union[int, None], 'url': OtpUrl})
|
||||
# PYTHON > 3.9: Otps = list[Otp]
|
||||
Otps = List[Otp]
|
||||
# PYTHON > 3.9: OtpUrls = list[OtpUrl]
|
||||
OtpUrls = List[OtpUrl]
|
||||
|
||||
QRMode = Enum('QRMode', ['QREADER', 'DEEP_QREADER', 'ZBAR', 'CV2', 'WECHAT'], start=0)
|
||||
|
||||
|
||||
# Constants
|
||||
CAMERA: Final[str] = 'camera'
|
||||
|
||||
# Global variable declaration
|
||||
verbose: int = 0
|
||||
quiet: bool = False
|
||||
colored: bool = True
|
||||
|
||||
|
||||
def sys_main() -> None:
|
||||
main(sys.argv[1:])
|
||||
|
||||
|
||||
def main(sys_args: list[str]) -> None:
|
||||
# allow to use sys.stdout with with (avoid closing)
|
||||
sys.stdout.close = lambda: None # type: ignore
|
||||
# set encoding to utf-8, needed for Windows
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding='utf-8') # type: ignore
|
||||
sys.stderr.reconfigure(encoding='utf-8') # type: ignore
|
||||
except AttributeError: # '_io.StringIO' object has no attribute 'reconfigure'
|
||||
# StringIO in tests do not have all attributes, ignore it
|
||||
pass
|
||||
|
||||
args = parse_args(sys_args)
|
||||
|
||||
if colored:
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
otps = extract_otps(args)
|
||||
write_csv(args, otps)
|
||||
write_keepass_csv(args, otps)
|
||||
write_json(args, otps)
|
||||
|
||||
|
||||
def parse_args(sys_args: list[str]) -> Args:
|
||||
global verbose, quiet, colored
|
||||
description_text = "Extracts one time password (OTP) secret keys from QR codes, e.g. from Google Authenticator app."
|
||||
if qreader_available:
|
||||
description_text += "\nIf no infiles are provided, the QR codes are interactively captured from the camera."
|
||||
example_text = """examples:
|
||||
python extract_otp_secrets.py
|
||||
python extract_otp_secrets.py example_*.txt
|
||||
python extract_otp_secrets.py - < example_export.txt
|
||||
python extract_otp_secrets.py --csv - example_*.png | tail -n+2
|
||||
python extract_otp_secrets.py = < example_export.png"""
|
||||
|
||||
arg_parser = argparse.ArgumentParser(formatter_class=lambda prog: argparse.RawTextHelpFormatter(prog, max_help_position=32),
|
||||
description=description_text,
|
||||
epilog=example_text)
|
||||
arg_parser.add_argument('infile', help="""a) file or - for stdin with 'otpauth-migration://...' URLs separated by newlines, lines starting with # are ignored;
|
||||
b) image file containing a QR code or = for stdin for an image containing a QR code""", nargs='*' if qreader_available else '+')
|
||||
if qreader_available:
|
||||
arg_parser.add_argument('--camera', '-C', help='camera number of system (default camera: 0)', default=0, type=int, metavar=('NUMBER'))
|
||||
arg_parser.add_argument('--qr', '-Q', help=f'QR reader (default: {QRMode.ZBAR.name})', type=str, choices=[mode.name for mode in QRMode], default=QRMode.ZBAR.name)
|
||||
arg_parser.add_argument('--json', '-j', help='export json file or - for stdout', metavar=('FILE'))
|
||||
arg_parser.add_argument('--csv', '-c', help='export csv file or - for stdout', metavar=('FILE'))
|
||||
arg_parser.add_argument('--keepass', '-k', help='export totp/hotp csv file(s) for KeePass, - for stdout', metavar=('FILE'))
|
||||
arg_parser.add_argument('--printqr', '-p', help='print QR code(s) as text to the terminal (requires qrcode module)', action='store_true')
|
||||
arg_parser.add_argument('--saveqr', '-s', help='save QR code(s) as images to the given folder (requires qrcode module)', metavar=('DIR'))
|
||||
arg_parser.add_argument('--no-color', '-n', help='do not use ANSI colors in console output', action='store_true')
|
||||
output_group = arg_parser.add_mutually_exclusive_group()
|
||||
output_group.add_argument('--verbose', '-v', help='verbose output', action='count')
|
||||
output_group.add_argument('--quiet', '-q', help='no stdout output, except output set by -', action='store_true')
|
||||
args = arg_parser.parse_args(sys_args)
|
||||
if args.csv == '-' or args.json == '-' or args.keepass == '-':
|
||||
args.quiet = args.q = True
|
||||
|
||||
verbose = args.verbose if args.verbose else 0
|
||||
quiet = True if args.quiet else False
|
||||
colored = not args.no_color
|
||||
if verbose: print(f"QReader installed: {qreader_available}")
|
||||
if qreader_available:
|
||||
if verbose > 1: print(f"CV2 version: {cv2.__version__}")
|
||||
if verbose: print(f"QR reading mode: {args.qr}\n")
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def extract_otps(args: Args) -> Otps:
|
||||
if not args.infile:
|
||||
return extract_otps_from_camera(args)
|
||||
else:
|
||||
return extract_otps_from_files(args)
|
||||
|
||||
|
||||
def get_color(new_otps_count: int, otp_url: str) -> ColorBGR:
|
||||
if new_otps_count:
|
||||
return SUCCESS_COLOR
|
||||
else:
|
||||
if otp_url:
|
||||
return FAILURE_COLOR
|
||||
else:
|
||||
return NORMAL_COLOR
|
||||
|
||||
|
||||
def extract_otps_from_camera(args: Args) -> Otps:
|
||||
if verbose: print("Capture QR codes from camera")
|
||||
otp_urls: OtpUrls = []
|
||||
otps: Otps = []
|
||||
|
||||
qr_mode = QRMode[args.qr]
|
||||
|
||||
cam = cv2.VideoCapture(args.camera)
|
||||
window_name = "Extract OTP Secrets: Capture QR Codes from Camera"
|
||||
cv2.namedWindow(window_name, cv2.WINDOW_AUTOSIZE)
|
||||
|
||||
qreader = QReader()
|
||||
cv2_qr = cv2.QRCodeDetector()
|
||||
cv2_qr_wechat = cv2.wechat_qrcode.WeChatQRCode()
|
||||
while True:
|
||||
success, img = cam.read()
|
||||
new_otps_count = 0
|
||||
if not success:
|
||||
log_error("Failed to capture image")
|
||||
break
|
||||
if qr_mode in [QRMode.QREADER, QRMode.DEEP_QREADER]:
|
||||
bbox, found = qreader.detect(img)
|
||||
if qr_mode == QRMode.DEEP_QREADER:
|
||||
otp_url = qreader.detect_and_decode(img, True)
|
||||
elif qr_mode == QRMode.QREADER:
|
||||
otp_url = qreader.decode(img, bbox) if found else None
|
||||
if otp_url:
|
||||
new_otps_count = extract_otps_from_otp_url(otp_url, otp_urls, otps, args)
|
||||
if found:
|
||||
cv2.rectangle(img, (bbox[0], bbox[1]), (bbox[2], bbox[3]), get_color(new_otps_count, otp_url), RECT_THICKNESS)
|
||||
elif qr_mode == QRMode.ZBAR:
|
||||
for qrcode in zbar.decode(img):
|
||||
otp_url = qrcode.data.decode('utf-8')
|
||||
pts = numpy.array([qrcode.polygon], numpy.int32)
|
||||
pts = pts.reshape((-1, 1, 2))
|
||||
new_otps_count = extract_otps_from_otp_url(otp_url, otp_urls, otps, args)
|
||||
cv2.polylines(img, [pts], True, get_color(new_otps_count, otp_url), RECT_THICKNESS)
|
||||
elif qr_mode in [QRMode.CV2, QRMode.WECHAT]:
|
||||
if QRMode.CV2:
|
||||
otp_url, raw_pts, _ = cv2_qr.detectAndDecode(img)
|
||||
else:
|
||||
otp_url, raw_pts = cv2_qr_wechat.detectAndDecode(img)
|
||||
if raw_pts is not None:
|
||||
if otp_url:
|
||||
new_otps_count = extract_otps_from_otp_url(otp_url, otp_urls, otps, args)
|
||||
pts = numpy.array([raw_pts], numpy.int32)
|
||||
pts = pts.reshape((-1, 1, 2))
|
||||
cv2.polylines(img, [pts], True, get_color(new_otps_count, otp_url), RECT_THICKNESS)
|
||||
else:
|
||||
assert False, f"Wrong QReader mode {qr_mode.name}"
|
||||
|
||||
cv2.putText(img, f"Mode: {qr_mode.name} (Hit space to change)", START_POS_TEXT, FONT, FONT_SCALE, NORMAL_COLOR, FONT_THICKNESS, FONT_LINE_STYLE)
|
||||
cv2.putText(img, "Hit ESC to quit", tuple(map(add, START_POS_TEXT, FONT_DY)), FONT, FONT_SCALE, NORMAL_COLOR, FONT_THICKNESS, FONT_LINE_STYLE)
|
||||
|
||||
window_dim = cv2.getWindowImageRect(window_name)
|
||||
qrcodes_text = f"{len(otp_urls)} QR code{'s'[:len(otp_urls) != 1]} captured"
|
||||
pos_qrcodes_text = window_dim[2] - cv2.getTextSize(qrcodes_text, FONT, FONT_SCALE, FONT_THICKNESS)[0][0] - 5, START_POS_TEXT[1]
|
||||
cv2.putText(img, qrcodes_text, pos_qrcodes_text, FONT, FONT_SCALE, NORMAL_COLOR, FONT_THICKNESS, FONT_LINE_STYLE)
|
||||
|
||||
otps_text = f"{len(otps)} otp{'s'[:len(otps) != 1]} extracted"
|
||||
pos_otps_text = window_dim[2] - cv2.getTextSize(otps_text, FONT, FONT_SCALE, FONT_THICKNESS)[0][0] - 5, START_POS_TEXT[1] + FONT_DY[1]
|
||||
cv2.putText(img, otps_text, pos_otps_text, FONT, FONT_SCALE, NORMAL_COLOR, FONT_THICKNESS, FONT_LINE_STYLE)
|
||||
cv2.imshow(window_name, img)
|
||||
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
if key == 27 or key == ord('q') or key == 13:
|
||||
# ESC or Enter or q pressed
|
||||
break
|
||||
elif key == 32:
|
||||
qr_mode = QRMode((qr_mode.value + 1) % len(QRMode))
|
||||
if verbose: print(f"QR reading mode: {qr_mode}")
|
||||
if cv2.getWindowProperty(window_name, cv2.WND_PROP_VISIBLE) < 1:
|
||||
# Window close clicked
|
||||
break
|
||||
|
||||
cam.release()
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
return otps
|
||||
|
||||
|
||||
def extract_otps_from_otp_url(otp_url: str, otp_urls: OtpUrls, otps: Otps, args: Args) -> int:
|
||||
'''Returns -1 if opt_url was already added.'''
|
||||
if otp_url and verbose: print(otp_url)
|
||||
if not otp_url:
|
||||
return 0
|
||||
if otp_url not in otp_urls:
|
||||
new_otps_count = extract_otp_from_otp_url(otp_url, otps, len(otp_urls), CAMERA, args)
|
||||
if new_otps_count:
|
||||
otp_urls.append(otp_url)
|
||||
if verbose: print(f"Extracted {new_otps_count} otp{'s'[:len(otps) != 1]}. {len(otps)} otp{'s'[:len(otps) != 1]} from {len(otp_urls)} QR code{'s'[:len(otp_urls) != 1]} extracted")
|
||||
return new_otps_count
|
||||
return -1
|
||||
|
||||
|
||||
def extract_otps_from_files(args: Args) -> Otps:
|
||||
otps: Otps = []
|
||||
|
||||
files_count = urls_count = otps_count = 0
|
||||
if verbose: print(f"Input files: {args.infile}")
|
||||
for infile in args.infile:
|
||||
if verbose: print(f"Processing infile {infile}")
|
||||
files_count += 1
|
||||
for line in get_otp_urls_from_file(infile, args):
|
||||
if verbose: print(line)
|
||||
if line.startswith('#') or line == '': continue
|
||||
urls_count += 1
|
||||
otps_count += extract_otp_from_otp_url(line, otps, urls_count, infile, args)
|
||||
if verbose: print(f"{files_count} infile{'s'[:files_count != 1]} processed")
|
||||
return otps
|
||||
|
||||
|
||||
def get_otp_urls_from_file(filename: str, args: Args) -> OtpUrls:
|
||||
# stdin stream cannot be rewinded, thus distinguish, use - for utf-8 stdin and = for binary image stdin
|
||||
if filename != '=':
|
||||
check_file_exists(filename)
|
||||
lines = read_lines_from_text_file(filename)
|
||||
if lines or filename == '-':
|
||||
return lines
|
||||
|
||||
# could not process text file, try reading as image
|
||||
if filename != '-' and qreader_available:
|
||||
return convert_img_to_otp_url(filename, args)
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def read_lines_from_text_file(filename: str) -> list[str]:
|
||||
if verbose: print(f"Reading lines of {filename}")
|
||||
# PYTHON >= 3.10 support encoding
|
||||
if sys.version_info >= (3, 10):
|
||||
finput = fileinput.input(filename, encoding='utf-8')
|
||||
else:
|
||||
finput = fileinput.input(filename)
|
||||
try:
|
||||
lines = []
|
||||
for line in (line.strip() for line in finput):
|
||||
if verbose: print(line)
|
||||
if is_binary(line):
|
||||
abort("Binary input was given in stdin, please use = instead of - as infile argument for images.")
|
||||
# unfortunately yield line leads to random test fails
|
||||
lines.append(line)
|
||||
if not lines:
|
||||
log_warn(f"{filename.replace('-', 'stdin')} is empty")
|
||||
except UnicodeDecodeError:
|
||||
if filename == '-':
|
||||
abort("Unable to open text file form stdin. "
|
||||
"In case you want read an image file from stdin, you must use '=' instead of '-'.")
|
||||
else: # The file is probably an image, process below
|
||||
return []
|
||||
finally:
|
||||
finput.close()
|
||||
return lines
|
||||
|
||||
|
||||
def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, urls_count: int, infile: str, args: Args) -> int:
|
||||
payload = get_payload_from_otp_url(otpauth_migration_url, urls_count, infile)
|
||||
|
||||
if not payload:
|
||||
return 0
|
||||
|
||||
new_otps_count = 0
|
||||
# pylint: disable=no-member
|
||||
for raw_otp in payload.otp_parameters:
|
||||
new_otps_count += 1
|
||||
if verbose: print(f"\n{len(otps) + 1}. Secret")
|
||||
secret = convert_secret_from_bytes_to_base32_str(raw_otp.secret)
|
||||
if verbose: print('OTP enum type:', get_enum_name_by_number(raw_otp, 'type'))
|
||||
otp_type = get_otp_type_str_from_code(raw_otp.type)
|
||||
otp_url = build_otp_url(secret, raw_otp)
|
||||
otp: Otp = {
|
||||
"name": raw_otp.name,
|
||||
"secret": secret,
|
||||
"issuer": raw_otp.issuer,
|
||||
"type": otp_type,
|
||||
"counter": raw_otp.counter if raw_otp.type == 1 else None,
|
||||
"url": otp_url
|
||||
}
|
||||
otps.append(otp)
|
||||
if not quiet:
|
||||
print_otp(otp)
|
||||
if args.printqr:
|
||||
print_qr(args, otp_url)
|
||||
if args.saveqr:
|
||||
save_qr(otp, args, len(otps))
|
||||
if not quiet:
|
||||
print()
|
||||
|
||||
return new_otps_count
|
||||
|
||||
|
||||
def convert_img_to_otp_url(filename: str, args: Args) -> OtpUrls:
|
||||
if verbose: print(f"Reading image {filename}")
|
||||
try:
|
||||
if filename != '=':
|
||||
img = cv2.imread(filename)
|
||||
else:
|
||||
try:
|
||||
stdin = sys.stdin.buffer.read()
|
||||
except AttributeError:
|
||||
# Workaround for pytest, since pytest cannot monkeypatch sys.stdin.buffer
|
||||
stdin = sys.stdin.read() # type: ignore # Workaround for pytest fixtures
|
||||
if not stdin:
|
||||
log_warn("stdin is empty")
|
||||
try:
|
||||
img_array = numpy.frombuffer(stdin, dtype='uint8')
|
||||
except TypeError as e:
|
||||
abort(f"Cannot read binary stdin buffer. Exception: {e}")
|
||||
if not img_array.size:
|
||||
return []
|
||||
img = cv2.imdecode(img_array, cv2.IMREAD_UNCHANGED)
|
||||
|
||||
if img is None:
|
||||
abort(f"Unable to open file for reading.\ninput file: {filename}")
|
||||
|
||||
qr_mode = QRMode[args.qr]
|
||||
otp_urls: OtpUrls = []
|
||||
if qr_mode in [QRMode.QREADER, QRMode.DEEP_QREADER]:
|
||||
otp_url = QReader().detect_and_decode(img, qr_mode == QRMode.DEEP_QREADER)
|
||||
otp_urls.append(otp_url)
|
||||
elif qr_mode == QRMode.CV2:
|
||||
otp_url, _, _ = cv2.QRCodeDetector().detectAndDecode(img)
|
||||
otp_urls.append(otp_url)
|
||||
elif qr_mode == QRMode.WECHAT:
|
||||
otp_url, _ = cv2.wechat_qrcode.WeChatQRCode().detectAndDecode(img)
|
||||
otp_urls += list(otp_url)
|
||||
elif qr_mode == QRMode.ZBAR:
|
||||
qrcodes = zbar.decode(img)
|
||||
otp_urls += [qrcode.data.decode('utf-8') for qrcode in qrcodes]
|
||||
else:
|
||||
assert False, f"Wrong QReader mode {qr_mode.name}"
|
||||
|
||||
if len(otp_urls) == 0:
|
||||
abort(f"Unable to read QR Code from file.\ninput file: {filename}")
|
||||
except Exception as e:
|
||||
abort(f"Encountered exception '{e}'.\ninput file: {filename}")
|
||||
return otp_urls
|
||||
|
||||
|
||||
# PYTHON >= 3.10 use: pb.MigrationPayload | None
|
||||
def get_payload_from_otp_url(otp_url: str, i: int, source: str) -> Optional[pb.MigrationPayload]:
|
||||
if not otp_url.startswith('otpauth-migration://'):
|
||||
msg = f"input is not a otpauth-migration:// url\nsource: {source}\ninput: {otp_url}"
|
||||
if source == CAMERA:
|
||||
log_error(f"{msg}")
|
||||
return None
|
||||
else:
|
||||
log_warn(f"{msg}\nMaybe a wrong file was given")
|
||||
parsed_url = urlparse.urlparse(otp_url)
|
||||
if verbose > 2: print(f"\nDEBUG: parsed_url={parsed_url}")
|
||||
try:
|
||||
params = urlparse.parse_qs(parsed_url.query, strict_parsing=True)
|
||||
except Exception: # Necessary for PYTHON < 3.11
|
||||
params = {}
|
||||
if verbose > 2: print(f"\nDEBUG: querystring params={params}")
|
||||
if 'data' not in params:
|
||||
log_error(f"could not parse query parameter in input url\nsource: {source}\nurl: {otp_url}")
|
||||
return None
|
||||
data_base64 = params['data'][0]
|
||||
if verbose > 2: print(f"\nDEBUG: data_base64={data_base64}")
|
||||
data_base64_fixed = data_base64.replace(' ', '+')
|
||||
if verbose > 2: print(f"\nDEBUG: data_base64_fixed={data_base64_fixed}")
|
||||
data = base64.b64decode(data_base64_fixed, validate=True)
|
||||
payload = pb.MigrationPayload()
|
||||
try:
|
||||
payload.ParseFromString(data)
|
||||
except Exception:
|
||||
abort(f"Cannot decode otpauth-migration migration payload.\n"
|
||||
f"data={data_base64}")
|
||||
if verbose:
|
||||
print(f"\n{i}. Payload Line", payload, sep='\n')
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
# https://stackoverflow.com/questions/40226049/find-enums-listed-in-python-descriptor-for-protobuf
|
||||
def get_enum_name_by_number(parent: Any, field_name: str) -> str:
|
||||
field_value = getattr(parent, field_name)
|
||||
return parent.DESCRIPTOR.fields_by_name[field_name].enum_type.values_by_number.get(field_value).name # type: ignore # generic code
|
||||
|
||||
|
||||
def get_otp_type_str_from_code(otp_type: int) -> str:
|
||||
return 'totp' if otp_type == 2 else 'hotp'
|
||||
|
||||
|
||||
def convert_secret_from_bytes_to_base32_str(bytes: bytes) -> str:
|
||||
return str(base64.b32encode(bytes), 'utf-8').replace('=', '')
|
||||
|
||||
|
||||
def build_otp_url(secret: str, raw_otp: pb.MigrationPayload.OtpParameters) -> str:
|
||||
url_params = {'secret': secret}
|
||||
if raw_otp.type == 1: url_params['counter'] = str(raw_otp.counter)
|
||||
if raw_otp.issuer: url_params['issuer'] = raw_otp.issuer
|
||||
otp_url = f"otpauth://{get_otp_type_str_from_code(raw_otp.type)}/{urlparse.quote(raw_otp.name)}?" + urlparse.urlencode(url_params)
|
||||
return otp_url
|
||||
|
||||
|
||||
def print_otp(otp: Otp) -> None:
|
||||
print(f"Name: {otp['name']}")
|
||||
print(f"Secret: {otp['secret']}")
|
||||
if otp['issuer']: print(f"Issuer: {otp['issuer']}")
|
||||
print(f"Type: {otp['type']}")
|
||||
if otp['type'] == 'hotp':
|
||||
print(f"Counter: {otp['counter']}")
|
||||
if verbose:
|
||||
print(otp['url'])
|
||||
|
||||
|
||||
def save_qr(otp: Otp, args: Args, j: int) -> str:
|
||||
dir = args.saveqr
|
||||
if not (os.path.exists(dir)): os.makedirs(dir, exist_ok=True)
|
||||
pattern = re.compile(r'[\W_]+')
|
||||
file_otp_name = pattern.sub('', otp['name'])
|
||||
file_otp_issuer = pattern.sub('', otp['issuer'])
|
||||
save_qr_file(args, otp['url'], f"{dir}/{j}-{file_otp_name}{'-' + file_otp_issuer if file_otp_issuer else ''}.png")
|
||||
return file_otp_name
|
||||
|
||||
|
||||
def save_qr_file(args: Args, otp_url: OtpUrl, name: str) -> None:
|
||||
qr = QRCode()
|
||||
qr.add_data(otp_url)
|
||||
img = qr.make_image(fill_color='black', back_color='white')
|
||||
if verbose: print(f"Saving to {name}")
|
||||
img.save(name)
|
||||
|
||||
|
||||
def print_qr(args: Args, otp_url: str) -> None:
|
||||
qr = QRCode()
|
||||
qr.add_data(otp_url)
|
||||
qr.print_ascii()
|
||||
|
||||
|
||||
def write_csv(args: Args, otps: Otps) -> None:
|
||||
if args.csv and len(otps) > 0:
|
||||
with open_file_or_stdout_for_csv(args.csv) as outfile:
|
||||
writer = csv.DictWriter(outfile, otps[0].keys())
|
||||
writer.writeheader()
|
||||
writer.writerows(otps)
|
||||
if not quiet: print(f"Exported {len(otps)} otp{'s'[:len(otps) != 1]} to csv {args.csv}")
|
||||
|
||||
|
||||
def write_keepass_csv(args: Args, otps: Otps) -> None:
|
||||
if args.keepass and len(otps) > 0:
|
||||
has_totp = has_otp_type(otps, 'totp')
|
||||
has_hotp = has_otp_type(otps, 'hotp')
|
||||
otp_filename_totp = args.keepass if has_totp != has_hotp else add_pre_suffix(args.keepass, "totp")
|
||||
otp_filename_hotp = args.keepass if has_totp != has_hotp else add_pre_suffix(args.keepass, "hotp")
|
||||
count_totp_entries = 0
|
||||
count_hotp_entries = 0
|
||||
if has_totp:
|
||||
with open_file_or_stdout_for_csv(otp_filename_totp) as outfile:
|
||||
writer = csv.DictWriter(outfile, ["Title", "User Name", "TimeOtp-Secret-Base32", "Group"])
|
||||
writer.writeheader()
|
||||
for otp in otps:
|
||||
if otp['type'] == 'totp':
|
||||
writer.writerow({
|
||||
'Title': otp['issuer'],
|
||||
'User Name': otp['name'],
|
||||
'TimeOtp-Secret-Base32': otp['secret'] if otp['type'] == 'totp' else None,
|
||||
'Group': f"OTP/{otp['type'].upper()}"
|
||||
})
|
||||
count_totp_entries += 1
|
||||
if has_hotp:
|
||||
with open_file_or_stdout_for_csv(otp_filename_hotp) as outfile:
|
||||
writer = csv.DictWriter(outfile, ["Title", "User Name", "HmacOtp-Secret-Base32", "HmacOtp-Counter", "Group"])
|
||||
writer.writeheader()
|
||||
for otp in otps:
|
||||
if otp['type'] == 'hotp':
|
||||
writer.writerow({
|
||||
'Title': otp['issuer'],
|
||||
'User Name': otp['name'],
|
||||
'HmacOtp-Secret-Base32': otp['secret'] if otp['type'] == 'hotp' else None,
|
||||
'HmacOtp-Counter': otp['counter'] if otp['type'] == 'hotp' else None,
|
||||
'Group': f"OTP/{otp['type'].upper()}"
|
||||
})
|
||||
count_hotp_entries += 1
|
||||
if not quiet:
|
||||
if count_totp_entries > 0: print(f"Exported {count_totp_entries} totp entrie{'s'[:count_totp_entries != 1]} to keepass csv file {otp_filename_totp}")
|
||||
if count_hotp_entries > 0: print(f"Exported {count_hotp_entries} hotp entrie{'s'[:count_hotp_entries != 1]} to keepass csv file {otp_filename_hotp}")
|
||||
|
||||
|
||||
def write_json(args: Args, otps: Otps) -> None:
|
||||
if args.json:
|
||||
with open_file_or_stdout(args.json) as outfile:
|
||||
json.dump(otps, outfile, indent=4)
|
||||
if not quiet: print(f"Exported {len(otps)} otp{'s'[:len(otps) != 1]} to json {args.json}")
|
||||
|
||||
|
||||
def has_otp_type(otps: Otps, otp_type: str) -> bool:
|
||||
for otp in otps:
|
||||
if otp['type'] == otp_type:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def add_pre_suffix(file: str, pre_suffix: str) -> str:
|
||||
'''filename.ext, pre -> filename.pre.ext'''
|
||||
name, ext = os.path.splitext(file)
|
||||
return name + "." + pre_suffix + (ext if ext else "")
|
||||
|
||||
|
||||
def open_file_or_stdout(filename: str) -> TextIO:
|
||||
'''stdout is denoted as "-".
|
||||
Note: Set before the following line:
|
||||
sys.stdout.close = lambda: None'''
|
||||
return open(filename, "w", encoding='utf-8') if filename != '-' else sys.stdout
|
||||
|
||||
|
||||
def open_file_or_stdout_for_csv(filename: str) -> TextIO:
|
||||
'''stdout is denoted as "-".
|
||||
newline=''
|
||||
Note: Set before the following line:
|
||||
sys.stdout.close = lambda: None'''
|
||||
return open(filename, "w", encoding='utf-8', newline='') if filename != '-' else sys.stdout
|
||||
|
||||
|
||||
def check_file_exists(filename: str) -> None:
|
||||
if filename != '-' and not os.path.isfile(filename):
|
||||
abort(f"Input file provided is non-existent or not a file."
|
||||
f"\ninput file: {filename}")
|
||||
|
||||
|
||||
def is_binary(line: str) -> bool:
|
||||
try:
|
||||
line.startswith('#')
|
||||
return False
|
||||
except (UnicodeDecodeError, AttributeError, TypeError):
|
||||
return True
|
||||
|
||||
|
||||
def log_warn(msg: str) -> None:
|
||||
eprint(f"{colorama.Fore.RED if colored else ''}\nWARN: {msg}{colorama.Fore.RESET if colored else ''}")
|
||||
|
||||
|
||||
def log_error(msg: str) -> None:
|
||||
eprint(f"{colorama.Fore.RED if colored else ''}\nERROR: {msg}{colorama.Fore.RESET if colored else ''}")
|
||||
|
||||
|
||||
def eprint(*args: Any, **kwargs: Any) -> None:
|
||||
'''Print to stderr.'''
|
||||
print(*args, file=sys.stderr, **kwargs)
|
||||
|
||||
|
||||
def abort(msg: str) -> None:
|
||||
log_error(msg)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys_main()
|
@ -1,32 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
// Copied from: https://github.com/beemdevelopment/Aegis/blob/master/app/src/main/proto/google_auth.proto
|
||||
|
||||
message MigrationPayload {
|
||||
enum Algorithm {
|
||||
ALGO_INVALID = 0;
|
||||
ALGO_SHA1 = 1;
|
||||
}
|
||||
|
||||
enum OtpType {
|
||||
OTP_INVALID = 0;
|
||||
OTP_HOTP = 1;
|
||||
OTP_TOTP = 2;
|
||||
}
|
||||
|
||||
message OtpParameters {
|
||||
bytes secret = 1;
|
||||
string name = 2;
|
||||
string issuer = 3;
|
||||
Algorithm algorithm = 4;
|
||||
int32 digits = 5;
|
||||
OtpType type = 6;
|
||||
int64 counter = 7;
|
||||
}
|
||||
|
||||
repeated OtpParameters otp_parameters = 1;
|
||||
int32 version = 2;
|
||||
int32 batch_size = 3;
|
||||
int32 batch_index = 4;
|
||||
int32 batch_id = 5;
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: google_auth.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf.internal import builder as _builder
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11google_auth.proto\"\xb7\x03\n\x10MigrationPayload\x12\x37\n\x0eotp_parameters\x18\x01 \x03(\x0b\x32\x1f.MigrationPayload.OtpParameters\x12\x0f\n\x07version\x18\x02 \x01(\x05\x12\x12\n\nbatch_size\x18\x03 \x01(\x05\x12\x13\n\x0b\x62\x61tch_index\x18\x04 \x01(\x05\x12\x10\n\x08\x62\x61tch_id\x18\x05 \x01(\x05\x1a\xb7\x01\n\rOtpParameters\x12\x0e\n\x06secret\x18\x01 \x01(\x0c\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0e\n\x06issuer\x18\x03 \x01(\t\x12.\n\talgorithm\x18\x04 \x01(\x0e\x32\x1b.MigrationPayload.Algorithm\x12\x0e\n\x06\x64igits\x18\x05 \x01(\x05\x12\'\n\x04type\x18\x06 \x01(\x0e\x32\x19.MigrationPayload.OtpType\x12\x0f\n\x07\x63ounter\x18\x07 \x01(\x03\",\n\tAlgorithm\x12\x10\n\x0c\x41LGO_INVALID\x10\x00\x12\r\n\tALGO_SHA1\x10\x01\"6\n\x07OtpType\x12\x0f\n\x0bOTP_INVALID\x10\x00\x12\x0c\n\x08OTP_HOTP\x10\x01\x12\x0c\n\x08OTP_TOTP\x10\x02\x62\x06proto3')
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google_auth_pb2', globals())
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
_MIGRATIONPAYLOAD._serialized_start=22
|
||||
_MIGRATIONPAYLOAD._serialized_end=461
|
||||
_MIGRATIONPAYLOAD_OTPPARAMETERS._serialized_start=176
|
||||
_MIGRATIONPAYLOAD_OTPPARAMETERS._serialized_end=359
|
||||
_MIGRATIONPAYLOAD_ALGORITHM._serialized_start=361
|
||||
_MIGRATIONPAYLOAD_ALGORITHM._serialized_end=405
|
||||
_MIGRATIONPAYLOAD_OTPTYPE._serialized_start=407
|
||||
_MIGRATIONPAYLOAD_OTPTYPE._serialized_end=461
|
||||
# @@protoc_insertion_point(module_scope)
|
@ -1,108 +0,0 @@
|
||||
"""
|
||||
@generated by mypy-protobuf. Do not edit manually!
|
||||
isort:skip_file
|
||||
"""
|
||||
import builtins
|
||||
import collections.abc
|
||||
import google.protobuf.descriptor
|
||||
import google.protobuf.internal.containers
|
||||
import google.protobuf.internal.enum_type_wrapper
|
||||
import google.protobuf.message
|
||||
import sys
|
||||
import typing
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
import typing as typing_extensions
|
||||
else:
|
||||
import typing_extensions
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
|
||||
|
||||
@typing_extensions.final
|
||||
class MigrationPayload(google.protobuf.message.Message):
|
||||
"""Copied from: https://github.com/beemdevelopment/Aegis/blob/master/app/src/main/proto/google_auth.proto"""
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
class _Algorithm:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
V: typing_extensions.TypeAlias = ValueType
|
||||
|
||||
class _AlgorithmEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[MigrationPayload._Algorithm.ValueType], builtins.type): # noqa: F821
|
||||
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
|
||||
ALGO_INVALID: MigrationPayload._Algorithm.ValueType # 0
|
||||
ALGO_SHA1: MigrationPayload._Algorithm.ValueType # 1
|
||||
|
||||
class Algorithm(_Algorithm, metaclass=_AlgorithmEnumTypeWrapper): ...
|
||||
ALGO_INVALID: MigrationPayload.Algorithm.ValueType # 0
|
||||
ALGO_SHA1: MigrationPayload.Algorithm.ValueType # 1
|
||||
|
||||
class _OtpType:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
V: typing_extensions.TypeAlias = ValueType
|
||||
|
||||
class _OtpTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[MigrationPayload._OtpType.ValueType], builtins.type): # noqa: F821
|
||||
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
|
||||
OTP_INVALID: MigrationPayload._OtpType.ValueType # 0
|
||||
OTP_HOTP: MigrationPayload._OtpType.ValueType # 1
|
||||
OTP_TOTP: MigrationPayload._OtpType.ValueType # 2
|
||||
|
||||
class OtpType(_OtpType, metaclass=_OtpTypeEnumTypeWrapper): ...
|
||||
OTP_INVALID: MigrationPayload.OtpType.ValueType # 0
|
||||
OTP_HOTP: MigrationPayload.OtpType.ValueType # 1
|
||||
OTP_TOTP: MigrationPayload.OtpType.ValueType # 2
|
||||
|
||||
@typing_extensions.final
|
||||
class OtpParameters(google.protobuf.message.Message):
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
SECRET_FIELD_NUMBER: builtins.int
|
||||
NAME_FIELD_NUMBER: builtins.int
|
||||
ISSUER_FIELD_NUMBER: builtins.int
|
||||
ALGORITHM_FIELD_NUMBER: builtins.int
|
||||
DIGITS_FIELD_NUMBER: builtins.int
|
||||
TYPE_FIELD_NUMBER: builtins.int
|
||||
COUNTER_FIELD_NUMBER: builtins.int
|
||||
secret: builtins.bytes
|
||||
name: builtins.str
|
||||
issuer: builtins.str
|
||||
algorithm: global___MigrationPayload.Algorithm.ValueType
|
||||
digits: builtins.int
|
||||
type: global___MigrationPayload.OtpType.ValueType
|
||||
counter: builtins.int
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
secret: builtins.bytes = ...,
|
||||
name: builtins.str = ...,
|
||||
issuer: builtins.str = ...,
|
||||
algorithm: global___MigrationPayload.Algorithm.ValueType = ...,
|
||||
digits: builtins.int = ...,
|
||||
type: global___MigrationPayload.OtpType.ValueType = ...,
|
||||
counter: builtins.int = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing_extensions.Literal["algorithm", b"algorithm", "counter", b"counter", "digits", b"digits", "issuer", b"issuer", "name", b"name", "secret", b"secret", "type", b"type"]) -> None: ...
|
||||
|
||||
OTP_PARAMETERS_FIELD_NUMBER: builtins.int
|
||||
VERSION_FIELD_NUMBER: builtins.int
|
||||
BATCH_SIZE_FIELD_NUMBER: builtins.int
|
||||
BATCH_INDEX_FIELD_NUMBER: builtins.int
|
||||
BATCH_ID_FIELD_NUMBER: builtins.int
|
||||
@property
|
||||
def otp_parameters(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MigrationPayload.OtpParameters]: ...
|
||||
version: builtins.int
|
||||
batch_size: builtins.int
|
||||
batch_index: builtins.int
|
||||
batch_id: builtins.int
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
otp_parameters: collections.abc.Iterable[global___MigrationPayload.OtpParameters] | None = ...,
|
||||
version: builtins.int = ...,
|
||||
batch_size: builtins.int = ...,
|
||||
batch_index: builtins.int = ...,
|
||||
batch_id: builtins.int = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing_extensions.Literal["batch_id", b"batch_id", "batch_index", b"batch_index", "batch_size", b"batch_size", "otp_parameters", b"otp_parameters", "version", b"version"]) -> None: ...
|
||||
|
||||
global___MigrationPayload = MigrationPayload
|
@ -1,15 +0,0 @@
|
||||
# 2FA example from https://www.raspberrypi.org/blog/setting-up-two-factor-authentication-on-your-raspberry-pi/
|
||||
# Secret key: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
otpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B
|
||||
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4%2F%2F%2F%2F%2FwE%3D
|
||||
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B
|
||||
|
||||
# otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
# Name: "encoding: ¿äÄéÉ? (demo)"
|
||||
otpauth-migration://offline?data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D
|
Binary file not shown.
@ -1,171 +0,0 @@
|
||||
QReader installed: True
|
||||
QR reading mode: ZBAR
|
||||
|
||||
Input files: ['example_export.txt']
|
||||
Processing infile example_export.txt
|
||||
Reading lines of example_export.txt
|
||||
# 2FA example from https://www.raspberrypi.org/blog/setting-up-two-factor-authentication-on-your-raspberry-pi/
|
||||
|
||||
# Secret key: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
otpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B
|
||||
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4%2F%2F%2F%2F%2FwE%3D
|
||||
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B
|
||||
|
||||
# otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||
otpauth-migration://offline?data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D
|
||||
|
||||
# otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
# Name: "encoding: ¿äÄéÉ? (demo)"
|
||||
otpauth-migration://offline?data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D
|
||||
# 2FA example from https://www.raspberrypi.org/blog/setting-up-two-factor-authentication-on-your-raspberry-pi/
|
||||
|
||||
# Secret key: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
otpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B
|
||||
|
||||
1. Payload Line
|
||||
otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "pi@raspberrypi"
|
||||
issuer: "raspberrypi"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_TOTP
|
||||
}
|
||||
version: 1
|
||||
batch_size: 1
|
||||
batch_id: -1320898453
|
||||
|
||||
|
||||
1. Secret
|
||||
OTP enum type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
|
||||
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4%2F%2F%2F%2F%2FwE%3D
|
||||
|
||||
2. Payload Line
|
||||
otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "pi@raspberrypi"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_TOTP
|
||||
}
|
||||
version: 1
|
||||
batch_size: 1
|
||||
batch_id: -2094403140
|
||||
|
||||
|
||||
2. Secret
|
||||
OTP enum type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B
|
||||
|
||||
3. Payload Line
|
||||
otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "pi@raspberrypi"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_TOTP
|
||||
}
|
||||
otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "pi@raspberrypi"
|
||||
issuer: "raspberrypi"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_TOTP
|
||||
}
|
||||
version: 1
|
||||
batch_size: 1
|
||||
batch_id: -1822886384
|
||||
|
||||
|
||||
3. Secret
|
||||
OTP enum type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
|
||||
4. Secret
|
||||
OTP enum type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
|
||||
|
||||
# otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||
otpauth-migration://offline?data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D
|
||||
|
||||
4. Payload Line
|
||||
otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "hotp demo"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_HOTP
|
||||
counter: 4
|
||||
}
|
||||
version: 1
|
||||
batch_size: 1
|
||||
batch_id: -1558849573
|
||||
|
||||
|
||||
5. Secret
|
||||
OTP enum type: OTP_HOTP
|
||||
Name: hotp demo
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: hotp
|
||||
Counter: 4
|
||||
otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||
|
||||
|
||||
# otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
# Name: "encoding: ¿äÄéÉ? (demo)"
|
||||
otpauth-migration://offline?data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D
|
||||
|
||||
5. Payload Line
|
||||
otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "encoding: ¿äÄéÉ? (demo)"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_TOTP
|
||||
}
|
||||
version: 1
|
||||
batch_size: 1
|
||||
batch_id: -171198419
|
||||
|
||||
|
||||
6. Secret
|
||||
OTP enum type: OTP_TOTP
|
||||
Name: encoding: ¿äÄéÉ? (demo)
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
1 infile processed
|
@ -1 +0,0 @@
|
||||
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
|
@ -1 +0,0 @@
|
||||
otpauth-migration://offline?data=XXXX
|
@ -1 +0,0 @@
|
||||
QR-Code:otpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B
|
@ -1 +0,0 @@
|
||||
otpauth-migration://offline?data=ClEKFAciUeGF4aS6IDCvMv99ySZ1ekKsEiVTZXJlbml0eUxhYnM6dGVzdDFAc2VyZW5pdHlsYWJzLmNvLnVrGgxTZXJlbml0eUxhYnMgASgBMAIKUQoUkIY8/fbrHZWTb4CBln18lvqt0HcSJVNlcmVuaXR5TGFiczp0ZXN0MkBzZXJlbml0eWxhYnMuY28udWsaDFNlcmVuaXR5TGFicyABKAEwAgpRChScf+1/Ua4d4gCY0W/7fj9VBkM9PBIlU2VyZW5pdHlMYWJzOnRlc3QzQHNlcmVuaXR5bGFicy5jby51axoMU2VyZW5pdHlMYWJzIAEoATACClEKFG6Qu0ryTSFA/l5rmvTIXtNeb5LtEiVTZXJlbml0eUxhYnM6dGVzdDRAc2VyZW5pdHlsYWJzLmNvLnVrGgxTZXJlbml0eUxhYnMgASgBMAIQARgBIAAogtTa1vz/////AQ==
|
@ -1 +0,0 @@
|
||||
This is just a text file masquerading as an image file.
|
@ -1,89 +0,0 @@
|
||||
# Unit test for extract_otp_secrets.py
|
||||
|
||||
# Run tests:
|
||||
# python -m unittest
|
||||
|
||||
# Author: sssudame
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations # for compatibility with PYTHON < 3.11
|
||||
import unittest
|
||||
|
||||
import extract_otp_secrets
|
||||
from utils import Capturing
|
||||
|
||||
|
||||
class TestQRImageExtract(unittest.TestCase):
|
||||
def test_img_qr_reader_happy_path(self) -> None:
|
||||
with Capturing() as actual_output:
|
||||
extract_otp_secrets.main(['tests/data/test_googleauth_export.png'])
|
||||
|
||||
expected_output =\
|
||||
['Name: Test1:test1@example1.com', 'Secret: JBSWY3DPEHPK3PXP', 'Issuer: Test1', 'Type: totp', '',
|
||||
'Name: Test2:test2@example2.com', 'Secret: JBSWY3DPEHPK3PXQ', 'Issuer: Test2', 'Type: totp', '',
|
||||
'Name: Test3:test3@example3.com', 'Secret: JBSWY3DPEHPK3PXR', 'Issuer: Test3', 'Type: totp', '']
|
||||
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
|
||||
def test_img_qr_reader_no_qr_code_in_image(self) -> None:
|
||||
with Capturing() as actual_output:
|
||||
with self.assertRaises(SystemExit) as context:
|
||||
extract_otp_secrets.main(['-n', 'tests/data/lena_std.tif'])
|
||||
|
||||
expected_output = ['', 'ERROR: Unable to read QR Code from file.', 'input file: tests/data/lena_std.tif']
|
||||
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
self.assertEqual(context.exception.code, 1)
|
||||
|
||||
def test_img_qr_reader_nonexistent_file(self) -> None:
|
||||
with Capturing() as actual_output:
|
||||
with self.assertRaises(SystemExit) as context:
|
||||
extract_otp_secrets.main(['-n', 'nonexistent.bmp'])
|
||||
|
||||
expected_output = ['', 'ERROR: Input file provided is non-existent or not a file.', 'input file: nonexistent.bmp']
|
||||
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
self.assertEqual(context.exception.code, 1)
|
||||
|
||||
def test_img_qr_reader_non_image_file(self) -> None:
|
||||
with Capturing() as actual_output:
|
||||
extract_otp_secrets.main(['-n', 'tests/data/text_masquerading_as_image.jpeg'])
|
||||
|
||||
expected_output = [
|
||||
'',
|
||||
'WARN: input is not a otpauth-migration:// url',
|
||||
'source: tests/data/text_masquerading_as_image.jpeg',
|
||||
"input: This is just a text file masquerading as an image file.",
|
||||
'Maybe a wrong file was given',
|
||||
'',
|
||||
'ERROR: could not parse query parameter in input url',
|
||||
'source: tests/data/text_masquerading_as_image.jpeg',
|
||||
"url: This is just a text file masquerading as an image file.",
|
||||
]
|
||||
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.cleanup()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,244 +0,0 @@
|
||||
# Unit test for extract_otp_secrets.py
|
||||
|
||||
# Run tests:
|
||||
# python -m unittest
|
||||
|
||||
# Author: Scito (https://scito.ch)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations # for compatibility with PYTHON < 3.11
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
import extract_otp_secrets
|
||||
from utils import (Capturing, read_csv, read_file_to_str, read_json,
|
||||
remove_dir_with_files, remove_file, count_files_in_dir)
|
||||
|
||||
|
||||
class TestExtract(unittest.TestCase):
|
||||
|
||||
def test_extract_csv(self) -> None:
|
||||
extract_otp_secrets.main(['-q', '-c', 'test_example_output.csv', 'example_export.txt'])
|
||||
|
||||
expected_csv = read_csv('example_output.csv')
|
||||
actual_csv = read_csv('test_example_output.csv')
|
||||
|
||||
self.assertEqual(actual_csv, expected_csv)
|
||||
|
||||
def test_extract_json(self) -> None:
|
||||
extract_otp_secrets.main(['-q', '-j', 'test_example_output.json', 'example_export.txt'])
|
||||
|
||||
expected_json = read_json('example_output.json')
|
||||
actual_json = read_json('test_example_output.json')
|
||||
|
||||
self.assertEqual(actual_json, expected_json)
|
||||
|
||||
def test_extract_stdout_1(self) -> None:
|
||||
with Capturing() as output:
|
||||
extract_otp_secrets.main(['example_export.txt'])
|
||||
|
||||
expected_output = [
|
||||
'Name: pi@raspberrypi',
|
||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||
'Issuer: raspberrypi',
|
||||
'Type: totp',
|
||||
'',
|
||||
'Name: pi@raspberrypi',
|
||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||
'Type: totp',
|
||||
'',
|
||||
'Name: pi@raspberrypi',
|
||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||
'Type: totp',
|
||||
'',
|
||||
'Name: pi@raspberrypi',
|
||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||
'Issuer: raspberrypi',
|
||||
'Type: totp',
|
||||
'',
|
||||
'Name: hotp demo',
|
||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||
'Type: hotp',
|
||||
'Counter: 4',
|
||||
'',
|
||||
'Name: encoding: ¿äÄéÉ? (demo)',
|
||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||
'Type: totp',
|
||||
''
|
||||
]
|
||||
self.assertEqual(output, expected_output)
|
||||
|
||||
# Ref for capturing https://stackoverflow.com/a/40984270
|
||||
def test_extract_stdout_2(self) -> None:
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
extract_otp_secrets.main(['example_export.txt'])
|
||||
actual_output = out.getvalue()
|
||||
|
||||
expected_output = '''Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
|
||||
Name: hotp demo
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: hotp
|
||||
Counter: 4
|
||||
|
||||
Name: encoding: ¿äÄéÉ? (demo)
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
|
||||
'''
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
|
||||
def test_extract_not_encoded_plus(self) -> None:
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
extract_otp_secrets.main(['tests/data/test_plus_problem_export.txt'])
|
||||
actual_output = out.getvalue()
|
||||
|
||||
expected_output = '''Name: SerenityLabs:test1@serenitylabs.co.uk
|
||||
Secret: A4RFDYMF4GSLUIBQV4ZP67OJEZ2XUQVM
|
||||
Issuer: SerenityLabs
|
||||
Type: totp
|
||||
|
||||
Name: SerenityLabs:test2@serenitylabs.co.uk
|
||||
Secret: SCDDZ7PW5MOZLE3PQCAZM7L4S35K3UDX
|
||||
Issuer: SerenityLabs
|
||||
Type: totp
|
||||
|
||||
Name: SerenityLabs:test3@serenitylabs.co.uk
|
||||
Secret: TR76272RVYO6EAEY2FX7W7R7KUDEGPJ4
|
||||
Issuer: SerenityLabs
|
||||
Type: totp
|
||||
|
||||
Name: SerenityLabs:test4@serenitylabs.co.uk
|
||||
Secret: N2ILWSXSJUQUB7S6NONPJSC62NPG7EXN
|
||||
Issuer: SerenityLabs
|
||||
Type: totp
|
||||
|
||||
'''
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
|
||||
def test_extract_printqr(self) -> None:
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
extract_otp_secrets.main(['-p', 'example_export.txt'])
|
||||
actual_output = out.getvalue()
|
||||
|
||||
expected_output = read_file_to_str('tests/data/printqr_output.txt')
|
||||
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
|
||||
def test_extract_saveqr(self) -> None:
|
||||
extract_otp_secrets.main(['-q', '-s', 'testout/qr/', 'example_export.txt'])
|
||||
|
||||
self.assertTrue(os.path.isfile('testout/qr/1-piraspberrypi-raspberrypi.png'))
|
||||
self.assertTrue(os.path.isfile('testout/qr/2-piraspberrypi.png'))
|
||||
self.assertTrue(os.path.isfile('testout/qr/3-piraspberrypi.png'))
|
||||
self.assertTrue(os.path.isfile('testout/qr/4-piraspberrypi-raspberrypi.png'))
|
||||
self.assertTrue(os.path.isfile('testout/qr/5-hotpdemo.png'))
|
||||
self.assertTrue(os.path.isfile('testout/qr/6-encodingäÄéÉdemo.png'))
|
||||
self.assertEqual(count_files_in_dir('testout/qr'), 6)
|
||||
|
||||
def test_extract_verbose(self) -> None:
|
||||
if sys.implementation.name == 'pypy' or sys.platform.startswith("win") or sys.version_info < (3, 10):
|
||||
self.skipTest("Avoid encoding problems")
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
extract_otp_secrets.main(['-n', '-v', 'example_export.txt'])
|
||||
actual_output = out.getvalue()
|
||||
|
||||
expected_output = read_file_to_str('tests/data/print_verbose_output.txt')
|
||||
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
|
||||
def test_extract_debug(self) -> None:
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
extract_otp_secrets.main(['-vvv', 'example_export.txt'])
|
||||
actual_output = out.getvalue()
|
||||
|
||||
expected_stdout = read_file_to_str('tests/data/print_verbose_output.txt')
|
||||
|
||||
self.assertGreater(len(actual_output), len(expected_stdout))
|
||||
self.assertTrue("DEBUG: " in actual_output)
|
||||
|
||||
def test_extract_help_1(self) -> None:
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
try:
|
||||
extract_otp_secrets.main(['-h'])
|
||||
self.fail("Must abort")
|
||||
except SystemExit as e:
|
||||
self.assertEqual(e.code, 0)
|
||||
|
||||
actual_output = out.getvalue()
|
||||
|
||||
self.assertGreater(len(actual_output), 0)
|
||||
self.assertTrue("-h, --help" in actual_output and "--verbose, -v" in actual_output)
|
||||
|
||||
def test_extract_help_2(self) -> None:
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
with self.assertRaises(SystemExit) as context:
|
||||
extract_otp_secrets.main(['-h'])
|
||||
|
||||
actual_output = out.getvalue()
|
||||
|
||||
self.assertGreater(len(actual_output), 0)
|
||||
self.assertTrue("-h, --help" in actual_output and "--verbose, -v" in actual_output)
|
||||
self.assertEqual(context.exception.code, 0)
|
||||
|
||||
def test_extract_help_3(self) -> None:
|
||||
with Capturing() as actual_output:
|
||||
with self.assertRaises(SystemExit) as context:
|
||||
extract_otp_secrets.main(['-h'])
|
||||
|
||||
self.assertGreater(len(actual_output), 0)
|
||||
self.assertTrue("-h, --help" in "\n".join(actual_output) and "--verbose, -v" in "\n".join(actual_output))
|
||||
self.assertEqual(context.exception.code, 0)
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.cleanup()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self) -> None:
|
||||
remove_file('test_example_output.csv')
|
||||
remove_file('test_example_output.json')
|
||||
remove_dir_with_files('testout/')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,140 +0,0 @@
|
||||
# Author: Scito (https://scito.ch)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations # for compatibility with PYTHON < 3.11
|
||||
import csv
|
||||
import glob
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import pathlib
|
||||
from typing import BinaryIO, Any, Union, List
|
||||
|
||||
|
||||
# Types
|
||||
# PYTHON < 3.10: Workaround for str | pathlib.Path
|
||||
PathLike = Union[str, pathlib.Path]
|
||||
|
||||
|
||||
# Ref. https://stackoverflow.com/a/16571630
|
||||
# PYTHON 3.11: class Capturing(list[Any]):
|
||||
class Capturing(List[Any]):
|
||||
'''Capture stdout and stderr
|
||||
Usage:
|
||||
with Capturing() as output:
|
||||
print("Output")
|
||||
'''
|
||||
# TODO remove type ignore if fixed, see https://github.com/python/mypy/issues/11871, https://stackoverflow.com/questions/72174409/type-hinting-the-return-value-of-a-class-method-that-returns-self
|
||||
def __enter__(self): # type: ignore
|
||||
self._stdout = sys.stdout
|
||||
sys.stdout = self._stringio_std = io.StringIO()
|
||||
self._stderr = sys.stderr
|
||||
sys.stderr = self._stringio_err = io.StringIO()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args: Any) -> None:
|
||||
self.extend(self._stringio_std.getvalue().splitlines())
|
||||
del self._stringio_std # free up some memory
|
||||
sys.stdout = self._stdout
|
||||
|
||||
self.extend(self._stringio_err.getvalue().splitlines())
|
||||
del self._stringio_err # free up some memory
|
||||
sys.stderr = self._stderr
|
||||
|
||||
|
||||
def file_exits(file: PathLike) -> bool:
|
||||
return os.path.isfile(file)
|
||||
|
||||
|
||||
def remove_file(file: PathLike) -> None:
|
||||
if file_exits(file): os.remove(file)
|
||||
|
||||
|
||||
def remove_files(glob_pattern: str) -> None:
|
||||
for f in glob.glob(glob_pattern):
|
||||
os.remove(f)
|
||||
|
||||
|
||||
def remove_dir_with_files(dir: PathLike) -> None:
|
||||
if os.path.exists(dir): shutil.rmtree(dir)
|
||||
|
||||
|
||||
def read_csv(filename: str) -> List[List[str]]:
|
||||
"""Returns a list of lines."""
|
||||
with open(filename, "r", encoding="utf-8", newline='') as infile:
|
||||
lines: List[List[str]] = []
|
||||
reader = csv.reader(infile)
|
||||
for line in reader:
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
|
||||
def read_csv_str(data_str: str) -> List[List[str]]:
|
||||
"""Returns a list of lines."""
|
||||
lines: List[List[str]] = []
|
||||
reader = csv.reader(data_str.splitlines())
|
||||
for line in reader:
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
|
||||
def read_json(filename: str) -> Any:
|
||||
"""Returns a list or a dictionary."""
|
||||
with open(filename, "r", encoding="utf-8") as infile:
|
||||
return json.load(infile)
|
||||
|
||||
|
||||
def read_json_str(data_str: str) -> Any:
|
||||
"""Returns a list or a dictionary."""
|
||||
return json.loads(data_str)
|
||||
|
||||
|
||||
def read_file_to_list(filename: str) -> List[str]:
|
||||
"""Returns a list of lines."""
|
||||
with open(filename, "r", encoding="utf-8") as infile:
|
||||
return infile.readlines()
|
||||
|
||||
|
||||
def read_file_to_str(filename: str) -> str:
|
||||
"""Returns a str."""
|
||||
return "".join(read_file_to_list(filename))
|
||||
|
||||
|
||||
def read_binary_file_as_stream(filename: str) -> BinaryIO:
|
||||
"""Returns binary file content."""
|
||||
with open(filename, "rb",) as infile:
|
||||
return io.BytesIO(infile.read())
|
||||
|
||||
|
||||
def replace_escaped_octal_utf8_bytes_with_str(str: str) -> str:
|
||||
encoded_name_strings = re.findall(r'name: .*$', str, flags=re.MULTILINE)
|
||||
for encoded_name_string in encoded_name_strings:
|
||||
escaped_bytes = re.findall(r'((?:\\[0-9]+)+)', encoded_name_string)
|
||||
for byte_sequence in escaped_bytes:
|
||||
unicode_str = b''.join([int(byte, 8).to_bytes(1, 'little') for byte in byte_sequence.split('\\') if byte]).decode('utf-8')
|
||||
print("Replace '{}' by '{}'".format(byte_sequence, unicode_str))
|
||||
str = str.replace(byte_sequence, unicode_str)
|
||||
return str
|
||||
|
||||
|
||||
def quick_and_dirty_workaround_encoding_problem(str: str) -> str:
|
||||
return re.sub(r'name: "encoding: .*$', '', str, flags=re.MULTILINE)
|
||||
|
||||
|
||||
def count_files_in_dir(path: PathLike) -> int:
|
||||
return len([name for name in os.listdir(path) if os.path.isfile(os.path.join(path, name))])
|
@ -1,351 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Upgrades Protoc from https://github.com/protocolbuffers/protobuf/releases
|
||||
|
||||
black='\e[0;30m'
|
||||
blackBold='\e[1;30m'
|
||||
blackBackground='\e[1;40m'
|
||||
red='\e[0;31m'
|
||||
redBold='\e[1;31m'
|
||||
redBackground='\e[0;41m'
|
||||
green='\e[0;32m'
|
||||
greenBold='\e[1;32m'
|
||||
greenBackground='\e[0;42m'
|
||||
yellow='\e[0;33m'
|
||||
yellowBold='\e[1;33m'
|
||||
yellowBackground='\e[0;43m'
|
||||
blue='\e[0;34m'
|
||||
blueBold='\e[1;34m'
|
||||
blueBackground='\e[0;44m'
|
||||
magenta='\e[0;35m'
|
||||
magentaBold='\e[1;35m'
|
||||
magentaBackground='\e[0;45m'
|
||||
cyan='\e[0;36m'
|
||||
cyanBold='\e[1;36m'
|
||||
cyanBackground='\e[0;46m'
|
||||
white='\e[0;37m'
|
||||
whiteBold='\e[1;37m'
|
||||
whiteBackground='\e[0;47m'
|
||||
reset='\e[0m'
|
||||
|
||||
abort() {
|
||||
echo '
|
||||
***************
|
||||
*** ABORTED ***
|
||||
***************
|
||||
' >&2
|
||||
echo "An error occurred on line $1. Exiting..." >&2
|
||||
date -Iseconds >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trap 'abort $LINENO' ERR
|
||||
set -e -o pipefail
|
||||
|
||||
quit() {
|
||||
trap : 0
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Asks if [Yn] if script shoud continue, otherwise exit 1
|
||||
# $1: msg or nothing
|
||||
# Example call 1: askContinueYn
|
||||
# Example call 1: askContinueYn "Backup DB?"
|
||||
askContinueYn() {
|
||||
if [[ $1 ]]; then
|
||||
msg="$1 "
|
||||
else
|
||||
msg=""
|
||||
fi
|
||||
|
||||
# http://stackoverflow.com/questions/3231804/in-bash-how-to-add-are-you-sure-y-n-to-any-command-or-alias
|
||||
read -e -p "${msg}Continue? [Y/n] " response
|
||||
response=${response,,} # tolower
|
||||
if [[ $response =~ ^(yes|y|)$ ]] ; then
|
||||
# echo ""
|
||||
# OK
|
||||
:
|
||||
else
|
||||
echo "Aborted"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Reference: https://gist.github.com/steinwaywhw/a4cd19cda655b8249d908261a62687f8
|
||||
|
||||
echo "Checking Protoc version..."
|
||||
VERSION=$(curl -sL https://github.com/protocolbuffers/protobuf/releases/latest | grep -E "<title>" | perl -pe's%.*Protocol Buffers v(\d+\.\d+(\.\d+)?).*%\1%')
|
||||
BASEVERSION=4
|
||||
echo
|
||||
|
||||
interactive=true
|
||||
ignore_version_check=true
|
||||
clean=false
|
||||
build_docker=true
|
||||
|
||||
while test $# -gt 0; do
|
||||
case $1 in
|
||||
-h|--help)
|
||||
echo "Upgrade Protoc"
|
||||
echo
|
||||
echo "$0 [options]"
|
||||
echo
|
||||
echo "Options:"
|
||||
echo "-a Automatic mode"
|
||||
echo "-C Ignore version check"
|
||||
echo "-D No docker build"
|
||||
echo "-c Clean"
|
||||
echo "-h, --help Help"
|
||||
quit
|
||||
;;
|
||||
-a)
|
||||
interactive=false
|
||||
shift
|
||||
;;
|
||||
-C)
|
||||
ignore_version_check=false
|
||||
shift
|
||||
;;
|
||||
-D)
|
||||
build_docker=false
|
||||
shift
|
||||
;;
|
||||
-c)
|
||||
clean=true
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
BIN="$HOME/bin"
|
||||
DOWNLOADS="$HOME/downloads"
|
||||
|
||||
PYTHON="python3.11"
|
||||
PIP="pip3.11"
|
||||
PIPENV="$PYTHON -m pipenv"
|
||||
FLAKE8="$PYTHON -m flake8"
|
||||
MYPY="$PYTHON -m mypy"
|
||||
|
||||
# Upgrade protoc
|
||||
|
||||
DEST="protoc"
|
||||
|
||||
OLDVERSION=$(cat $BIN/$DEST/.VERSION.txt || echo "")
|
||||
echo -e "\nProtoc remote version $VERSION\n"
|
||||
echo -e "Protoc local version: $OLDVERSION\n"
|
||||
|
||||
if $clean; then
|
||||
cmd="rm -r dist/ build/ *.whl pytest.xml pytest-coverage.txt .coverage tests/reports || true; find . -name '*.pyc' -type f -delete; find . -name '__pycache__' -type d -exec rm -r {} \; || true; find . -name '*.egg-info' -type d -exec rm -r {} \; || true; find . -name '*_cache' -type d -exec rm -r {} \; || true"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="pipenv --rm || true"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="sudo pipenv --rm || true"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
if [ "$OLDVERSION" != "$VERSION" ] || ! $ignore_version_check; then
|
||||
echo "Upgrade protoc from $OLDVERSION to $VERSION"
|
||||
|
||||
NAME="protoc-$VERSION"
|
||||
ARCHIVE="$NAME.zip"
|
||||
|
||||
mkdir -p $DOWNLOADS
|
||||
# https://github.com/protocolbuffers/protobuf/releases/download/v21.6/protoc-21.6-linux-x86_64.zip
|
||||
cmd="wget --trust-server-names https://github.com/protocolbuffers/protobuf/releases/download/v$VERSION/protoc-$VERSION-linux-x86_64.zip -O $DOWNLOADS/$ARCHIVE"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="echo -e '\nSize [Byte]'; stat --printf='%s\n' $DOWNLOADS/$ARCHIVE; echo -e '\nMD5'; md5sum $DOWNLOADS/$ARCHIVE; echo -e '\nSHA256'; sha256sum $DOWNLOADS/$ARCHIVE;"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="mkdir -p $BIN/$NAME; unzip $DOWNLOADS/$ARCHIVE -d $BIN/$NAME"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="echo $VERSION > $BIN/$NAME/.VERSION.txt; echo $VERSION > $BIN/$NAME/.VERSION_$VERSION.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="[ -d $BIN/$DEST.old ] && rm -rf $BIN/$DEST.old || echo 'No old dir to delete'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="[ -d $BIN/$DEST ] && mv -iT $BIN/$DEST $BIN/$DEST.old || echo 'No previous dir to keep'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="mv -iT $BIN/$NAME $BIN/$DEST"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="rm $DOWNLOADS/$ARCHIVE"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$BIN/$DEST/bin/protoc --plugin=protoc-gen-mypy=$HOME/.local/bin/protoc-gen-mypy --python_out=protobuf_generated_python --mypy_out=protobuf_generated_python src/google_auth.proto"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Update README.md
|
||||
|
||||
cmd="perl -i -pe 's%proto(buf|c)([- ])(\d\.)?$OLDVERSION%proto\$1\$2\${3}$VERSION%g' README.md && perl -i -pe 's%(protobuf/releases/tag/v)$OLDVERSION%\${1}$VERSION%g' README.md"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
else
|
||||
echo -e "\nVersion has not changed. Quit"
|
||||
fi
|
||||
|
||||
|
||||
# Upgrade pip requirements
|
||||
|
||||
cmd="sudo pip install --upgrade pip"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
$PIP --version
|
||||
|
||||
cmd="$PIP install --use-pep517 -U -r requirements.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIP install --use-pep517 -U -r requirements-dev.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIP install -U pipenv"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="sudo $PIP install --use-pep517 -U -r requirements.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="sudo $PIP install --use-pep517 -U -r requirements-dev.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="sudo $PIP install -U pipenv"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
$PIPENV --version
|
||||
|
||||
cmd="$PIPENV update && $PIPENV --rm && $PIPENV install"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
$PIPENV run python --version
|
||||
|
||||
# Lint
|
||||
|
||||
cmd="$FLAKE8 . --count --select=E9,F63,F7,F82 --show-source --statistics"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$FLAKE8 . --count --exit-zero --max-complexity=10 --max-line-length=200 --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,protobuf_generated_python"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Type checking
|
||||
|
||||
cmd="$MYPY --install-types --non-interactive src/*.py tests/*.py"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# change to src as python -m mypy adds the current dir Python sys.path
|
||||
# execute in a subshell in order not to loose the exit code and not to change the dir in the currrent shell
|
||||
cmd="$MYPY --strict src/*.py tests/*.py"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# pip install
|
||||
|
||||
cmd="$PIP install -e ."
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="extract_otp_secrets example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="extract_otp_secrets - < example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Test
|
||||
|
||||
cmd="$PYTHON src/extract_otp_secrets.py example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PYTHON src/extract_otp_secrets.py - < example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
COVERAGE_OUT="tests/reports/pytest-coverage.txt"
|
||||
cmd="mkdir -p tests/reports; pytest --cov=extract_otp_secrets_test --junitxml=tests/reports/pytest.xml --cov-report html:tests/reports/html --cov-report=term-missing tests/ | tee $COVERAGE_OUT"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIPENV run pytest --cov=extract_otp_secrets_test tests/"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Update Code Coverage in README.md
|
||||
|
||||
# https://github.com/marketplace/actions/pytest-coverage-comment
|
||||
# Coverage-95%25-yellowgreen
|
||||
echo -e "Update code coverage in README.md"
|
||||
TOTAL_COVERAGE=$(cat $COVERAGE_OUT | grep 'TOTAL' | perl -ne 'print "$&" if /\b(\d{1,3})%/') && perl -i -pe "s/coverage-(\d{1,3}%)25-/coverage-${TOTAL_COVERAGE}25-/" README.md
|
||||
|
||||
if $build_docker; then
|
||||
# Build docker
|
||||
|
||||
cmd="docker build . -t extract_otp_secrets_only_txt -f Dockerfile_only_txt --pull"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker run --rm -v \"$(pwd)\":/files:ro extract_otp_secrets_only_txt example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker run --rm -i -v \"$(pwd)\":/files:ro extract_otp_secrets_only_txt - < example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker run --entrypoint /extract/run_pytest.sh --rm -v \"$(pwd)\":/files:ro extract_otp_secrets_only_txt tests/extract_otp_secrets_test.py -k 'not qreader' -vvv --relaxed"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker build . -t extract_otp_secrets --pull"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker run --rm -v \"$(pwd)\":/files:ro extract_otp_secrets example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker run --rm -i -v \"$(pwd)\":/files:ro extract_otp_secrets - < example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker run --entrypoint /extract/run_pytest.sh --rm -v \"$(pwd)\":/files:ro extract_otp_secrets"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker image prune || echo 'No docker image pruning'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
cmd="$PYTHON src/extract_otp_secrets.py &"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
quit
|
Loading…
Reference in New Issue