Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
445d77783c | ||
|
|
f0134fa907 | ||
|
|
e0588285c9 | ||
|
|
ff9401687e | ||
|
|
2478edb7a1 | ||
|
|
5e439b9396 | ||
|
|
5c4d3ce696 | ||
|
|
ec09b5daad | ||
|
|
2ed923591e | ||
|
|
36fd0c0bb6 | ||
|
|
b215b78dad | ||
|
|
851cb6532c | ||
|
|
2bef64e5f6 | ||
|
|
3502294172 | ||
|
|
2707e244be | ||
|
|
4ba0fad000 | ||
|
|
9d052dc78a | ||
|
|
915efcf192 |
9
.github/workflows/ci.yml
vendored
@@ -2,10 +2,12 @@ name: tests
|
||||
|
||||
# https://docs.github.com/de/actions/using-workflows/workflow-syntax-for-github-actions
|
||||
# https://docs.github.com/en/actions/using-workflows
|
||||
# https://docs.github.com/en/actions/learn-github-actions/contexts
|
||||
# https://docs.github.com/en/actions/learn-github-actions/expressions
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
# pull_request:
|
||||
schedule:
|
||||
# Run daily on default branch
|
||||
- cron: '37 3 * * *'
|
||||
@@ -65,4 +67,7 @@ jobs:
|
||||
with:
|
||||
pytest-coverage-path: ./pytest-coverage.txt
|
||||
junitxml-path: ./pytest.xml
|
||||
if: matrix.python-version == '3.x' && matrix.platform == 'ubuntu-latest'
|
||||
if: |
|
||||
matrix.python-version == '3.x' && matrix.platform == 'ubuntu-latest'
|
||||
&& !contains(github.ref, 'refs/tags/')
|
||||
|
||||
|
||||
3
.github/workflows/ci_docker.yml
vendored
@@ -2,6 +2,8 @@ name: docker
|
||||
|
||||
# https://docs.github.com/de/actions/using-workflows/workflow-syntax-for-github-actions
|
||||
# https://docs.github.com/en/actions/using-workflows
|
||||
# https://docs.github.com/en/actions/learn-github-actions/contexts
|
||||
# https://docs.github.com/en/actions/learn-github-actions/expressions
|
||||
|
||||
# How to setup: https://event-driven.io/en/how_to_buid_and_push_docker_image_with_github_actions/
|
||||
# How to run: https://aschmelyun.com/blog/using-docker-run-inside-of-github-actions/
|
||||
@@ -9,7 +11,6 @@ name: docker
|
||||
on:
|
||||
# run it on push to the default repository branch
|
||||
push:
|
||||
branches: [master]
|
||||
schedule:
|
||||
# Run weekly on default branch
|
||||
- cron: '47 3 * * 6'
|
||||
|
||||
22
Pipfile
@@ -4,24 +4,26 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
protobuf = "*"
|
||||
qrcode = "*"
|
||||
pillow = "*"
|
||||
qreader = "*"
|
||||
opencv-contrib-python = "*"
|
||||
colorama = ">=0.4.6"
|
||||
opencv-contrib-python = "*"
|
||||
# for macOS: opencv-contrib-python = "<=4.7.0"
|
||||
# for PYTHON <= 3.7: typing_extensions = "*"
|
||||
pillow = "*"
|
||||
protobuf = "*"
|
||||
qrcode = "*"
|
||||
qreader = "<2.0.0"
|
||||
|
||||
[dev-packages]
|
||||
pytest = "*"
|
||||
pytest-mock = "*"
|
||||
pytest-cov = "*"
|
||||
wheel = "*"
|
||||
build = "*"
|
||||
flake8 = "*"
|
||||
pylint = "*"
|
||||
mypy = "*"
|
||||
mypy-protobuf = "*"
|
||||
pylint = "*"
|
||||
pytest = "*"
|
||||
pytest-cov = "*"
|
||||
pytest-mock = "*"
|
||||
types-protobuf = "*"
|
||||
wheel = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.11"
|
||||
|
||||
254
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "25b244c44cb891ac15ef20c4011eb043b87fb1f112396d68f470d0bb362e97f7"
|
||||
"sha256": "9b56c9708e464fbb035e36b29b0c89f0250bc50ac37234f3948a366136a8e6c9"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -93,6 +93,7 @@
|
||||
},
|
||||
"pillow": {
|
||||
"hashes": [
|
||||
"sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33",
|
||||
"sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b",
|
||||
"sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e",
|
||||
"sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35",
|
||||
@@ -101,9 +102,12 @@
|
||||
"sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569",
|
||||
"sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57",
|
||||
"sha256:19005a8e58b7c1796bc0167862b1f54a64d3b44ee5d48152b06bb861458bc0f8",
|
||||
"sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1",
|
||||
"sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264",
|
||||
"sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157",
|
||||
"sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9",
|
||||
"sha256:3fa1284762aacca6dc97474ee9c16f83990b8eeb6697f2ba17140d54b453e133",
|
||||
"sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9",
|
||||
"sha256:451f10ef963918e65b8869e17d67db5e2f4ab40e716ee6ce7129b0cde2876eab",
|
||||
"sha256:46c259e87199041583658457372a183636ae8cd56dbf3f0755e0f376a7f9d0e6",
|
||||
"sha256:46f39cab8bbf4a384ba7cb0bc8bae7b7062b6a11cfac1ca4bc144dea90d4a9f5",
|
||||
@@ -123,10 +127,16 @@
|
||||
"sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a",
|
||||
"sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e",
|
||||
"sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f",
|
||||
"sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848",
|
||||
"sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57",
|
||||
"sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f",
|
||||
"sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c",
|
||||
"sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9",
|
||||
"sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5",
|
||||
"sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9",
|
||||
"sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d",
|
||||
"sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0",
|
||||
"sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1",
|
||||
"sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e",
|
||||
"sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815",
|
||||
"sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0",
|
||||
@@ -134,6 +144,8 @@
|
||||
"sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd",
|
||||
"sha256:b222090c455d6d1a64e6b7bb5f4035c4dff479e22455c9eaa1bdd4c75b52c80c",
|
||||
"sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3",
|
||||
"sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab",
|
||||
"sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858",
|
||||
"sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5",
|
||||
"sha256:ba6612b6548220ff5e9df85261bddc811a057b0b465a1226b39bfb8550616aee",
|
||||
"sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343",
|
||||
@@ -154,8 +166,10 @@
|
||||
"sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070",
|
||||
"sha256:ef21af928e807f10bf4141cad4746eee692a0dd3ff56cfb25fce076ec3cc8abe",
|
||||
"sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a",
|
||||
"sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd",
|
||||
"sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391",
|
||||
"sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a"
|
||||
"sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a",
|
||||
"sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==9.4.0"
|
||||
@@ -206,11 +220,11 @@
|
||||
"develop": {
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
"sha256:10e0ad5f7b79c435179d0d0f0df69998c4eef4597534aae44910db060baeb907",
|
||||
"sha256:1493fe8bd3dfd73dc35bd53c9d5b6e49ead98497c47b2307662556a5692d29d7"
|
||||
"sha256:3bc7834720e1a24ca797fd785d77efb14f7a28ee8e635ef040b6e2d80ccb3303",
|
||||
"sha256:8f6a8d40c4ad161d6fc419545ae4b2f275ed86d1c989c97825772120842ee0d2"
|
||||
],
|
||||
"markers": "python_full_version >= '3.7.2'",
|
||||
"version": "==2.12.13"
|
||||
"version": "==2.13.2"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
@@ -220,65 +234,73 @@
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==22.2.0"
|
||||
},
|
||||
"build": {
|
||||
"hashes": [
|
||||
"sha256:af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171",
|
||||
"sha256:d5b71264afdb5951d6704482aac78de887c80691c52b88a9ad195983ca2c9269"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"coverage": {
|
||||
"extras": [
|
||||
"toml"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:04691b8e832a900ed15f5bcccc2008fc2d1c8e7411251fd7d1422a84e1d72841",
|
||||
"sha256:1a613d60be1a02c7a5184ea5c4227f48c08e0635608b9c17ae2b17efef8f2501",
|
||||
"sha256:1d732b5dcafed67d81c5b5a0c404c31a61e13148946a3b910a340f72fdd1ec95",
|
||||
"sha256:2b31f7f246dbff339b3b76ee81329e3eca5022ce270c831c65e841dbbb40115f",
|
||||
"sha256:312fd77258bf1044ef4faa82091f2e88216e4b62dcf1a461d3e917144c8b09b7",
|
||||
"sha256:321316a7b979892a13c148a9d37852b5a76f26717e4b911b606a649394629532",
|
||||
"sha256:36c1a1b6d38ebf8a4335f65226ec36b5d6fd67743fdcbad5c52bdcd46c4f5842",
|
||||
"sha256:38f281bb9bdd4269c451fed9451203512dadefd64676f14ed1e82c77eb5644fc",
|
||||
"sha256:3a2d81c95d3b02638ee6ae647edc79769fd29bf5e9e5b6b0c29040579f33c260",
|
||||
"sha256:3d40ad86a348c79c614e2b90566267dd6d45f2e6b4d2bfb794d78ea4a4b60b63",
|
||||
"sha256:3d72e3d20b03e63bd27b1c4d6b754cd93eca82ecc5dd77b99262d5f64862ca35",
|
||||
"sha256:3fbb59f84c8549113dcdce7c6d16c5731fe53651d0b46c0a25a5ebc7bb655869",
|
||||
"sha256:405d8528a0ea07ca516d9007ecad4e1bd10e2eeef27411c6188d78c4e2dfcddc",
|
||||
"sha256:420f10c852b9a489cf5a764534669a19f49732a0576c76d9489ebf287f81af6d",
|
||||
"sha256:426895ac9f2938bec193aa998e7a77a3e65d3e46903f348e794b4192b9a5b61e",
|
||||
"sha256:4438ba539bef21e288092b30ea2fc30e883d9af5b66ebeaf2fd7c25e2f074e39",
|
||||
"sha256:46db409fc0c3ee5c859b84c7de9cb507166287d588390889fdf06a1afe452e16",
|
||||
"sha256:483e120ea324c7fced6126bb9bf0535c71e9233d29cbc7e2fc4560311a5f8a32",
|
||||
"sha256:4d7be755d7544dac2b9814e98366a065d15a16e13847eb1f5473bb714483391e",
|
||||
"sha256:4e97b21482aa5c21e049e4755c95955ad71fb54c9488969e2f17cf30922aa5f6",
|
||||
"sha256:5f44ba7c07e0aa4a7a2723b426c254e952da82a33d65b4a52afae4bef74a4203",
|
||||
"sha256:62e5b942378d5f0b87caace567a70dc634ddd4d219a236fa221dc97d2fc412c8",
|
||||
"sha256:7c669be1b01e4b2bf23aa49e987d9bedde0234a7da374a9b77ca5416d7c57002",
|
||||
"sha256:7d47d666e17e57ef65fefc87229fde262bd5c9039ae8424bc53aa2d8f07dc178",
|
||||
"sha256:7e184aa18f921b612ea08666c25dd92a71241c8ed40917f2824219c92289b8c7",
|
||||
"sha256:80583c536e7e010e301002088919d4ea90566d1789ee02551574fdf3faa275ae",
|
||||
"sha256:8217f73faf08623acb25fb2affd5d20cbcd8185213db308e46a37e6fd6a56a49",
|
||||
"sha256:87d95eea58fb71f69b4f1c761099a19e0e9cb27d45dc1cc7042523128ee56337",
|
||||
"sha256:8bd466135fb07f693dbdd999a5569ffbc0590e9c64df859243162f0ebee950c8",
|
||||
"sha256:8e133ca2f8141b415ff396ba789bdeffdea8ff9a5c7fc9996ccf591d7d40ee93",
|
||||
"sha256:8e6c0ca447b557a32642f22d0987be37950eda51c4f19fc788cebc99426284b6",
|
||||
"sha256:9de96025ce25b9f4e744fbe558a003e673004af255da9b1f6ec243720ac5deeb",
|
||||
"sha256:a27a8dca0dc6f0944ed9fd83c556d862e227a5cd4220e62af5d4c750389938f0",
|
||||
"sha256:a2d4f68e4fa286fb6b00d58a1e87c79840e289d3f6e5dcb912ad7b0fbd9629fb",
|
||||
"sha256:a6e1c77ff6f10eab496fbbcdaa7dfae84968928a0aadc43ce3c3453cec29bd79",
|
||||
"sha256:a7b018811a0e1d3869d8d0600849953acd355a3a29c6bee0fbd24d7772bcc0a2",
|
||||
"sha256:a99b2f2dd1236e8d9dc35974a3dc298a408cdfd512b0bb2451798cff1ce63408",
|
||||
"sha256:ac1033942851bf01f28c76318155ea92d6648aecb924cab81fa23781d095e9ab",
|
||||
"sha256:b6936cd38757dd323fefc157823e46436610328f0feb1419a412316f24b77f36",
|
||||
"sha256:b6eab230b18458707b5c501548e997e42934b1c189fb4d1b78bf5aacc1c6a137",
|
||||
"sha256:bcb57d175ff0cb4ff97fc547c74c1cb8d4c9612003f6d267ee78dad8f23d8b30",
|
||||
"sha256:c1f02d016b9b6b5ad21949a21646714bfa7b32d6041a30f97674f05d6d6996e3",
|
||||
"sha256:c40aaf7930680e0e5f3bd6d3d3dc97a7897f53bdce925545c4b241e0c5c3ca6a",
|
||||
"sha256:c5e1874c601128cf54c1d4b471e915658a334fbc56d7b3c324ddc7511597ea82",
|
||||
"sha256:c8805673b1953313adfc487d5323b4c87864e77057944a0888c98dd2f7a6052f",
|
||||
"sha256:da458bdc9b0bcd9b8ca85bc73148631b18cc8ba03c47f29f4c017809990351aa",
|
||||
"sha256:dcb708ab06f3f4dfc99e9f84821c9120e5f12113b90fad132311a2cb97525379",
|
||||
"sha256:dfafc350f43fd7dc67df18c940c3b7ed208ebb797abe9fb3047f0c65be8e4c0f",
|
||||
"sha256:e8931af864bd599c6af626575a02103ae626f57b34e3af5537d40b040d33d2ad",
|
||||
"sha256:efa9d943189321f67f71070c309aa6f6130fa1ec35c9dfd0da0ed238938ce573",
|
||||
"sha256:fd22ee7bff4b5c37bb6385efee1c501b75e29ca40286f037cb91c2182d1348ce"
|
||||
"sha256:051afcbd6d2ac39298d62d340f94dbb6a1f31de06dfaf6fcef7b759dd3860c45",
|
||||
"sha256:0a1890fca2962c4f1ad16551d660b46ea77291fba2cc21c024cd527b9d9c8809",
|
||||
"sha256:0ee30375b409d9a7ea0f30c50645d436b6f5dfee254edffd27e45a980ad2c7f4",
|
||||
"sha256:13250b1f0bd023e0c9f11838bdeb60214dd5b6aaf8e8d2f110c7e232a1bff83b",
|
||||
"sha256:17e01dd8666c445025c29684d4aabf5a90dc6ef1ab25328aa52bedaa95b65ad7",
|
||||
"sha256:19245c249aa711d954623d94f23cc94c0fd65865661f20b7781210cb97c471c0",
|
||||
"sha256:1caed2367b32cc80a2b7f58a9f46658218a19c6cfe5bc234021966dc3daa01f0",
|
||||
"sha256:1f66862d3a41674ebd8d1a7b6f5387fe5ce353f8719040a986551a545d7d83ea",
|
||||
"sha256:220e3fa77d14c8a507b2d951e463b57a1f7810a6443a26f9b7591ef39047b1b2",
|
||||
"sha256:276f4cd0001cd83b00817c8db76730938b1ee40f4993b6a905f40a7278103b3a",
|
||||
"sha256:29de916ba1099ba2aab76aca101580006adfac5646de9b7c010a0f13867cba45",
|
||||
"sha256:2a7f23bbaeb2a87f90f607730b45564076d870f1fb07b9318d0c21f36871932b",
|
||||
"sha256:2c407b1950b2d2ffa091f4e225ca19a66a9bd81222f27c56bd12658fc5ca1209",
|
||||
"sha256:30b5fec1d34cc932c1bc04017b538ce16bf84e239378b8f75220478645d11fca",
|
||||
"sha256:3c2155943896ac78b9b0fd910fb381186d0c345911f5333ee46ac44c8f0e43ab",
|
||||
"sha256:411d4ff9d041be08fdfc02adf62e89c735b9468f6d8f6427f8a14b6bb0a85095",
|
||||
"sha256:436e103950d05b7d7f55e39beeb4d5be298ca3e119e0589c0227e6d0b01ee8c7",
|
||||
"sha256:49640bda9bda35b057b0e65b7c43ba706fa2335c9a9896652aebe0fa399e80e6",
|
||||
"sha256:4a950f83fd3f9bca23b77442f3a2b2ea4ac900944d8af9993743774c4fdc57af",
|
||||
"sha256:50a6adc2be8edd7ee67d1abc3cd20678987c7b9d79cd265de55941e3d0d56499",
|
||||
"sha256:52ab14b9e09ce052237dfe12d6892dd39b0401690856bcfe75d5baba4bfe2831",
|
||||
"sha256:54f7e9705e14b2c9f6abdeb127c390f679f6dbe64ba732788d3015f7f76ef637",
|
||||
"sha256:66e50680e888840c0995f2ad766e726ce71ca682e3c5f4eee82272c7671d38a2",
|
||||
"sha256:790e4433962c9f454e213b21b0fd4b42310ade9c077e8edcb5113db0818450cb",
|
||||
"sha256:7a38362528a9115a4e276e65eeabf67dcfaf57698e17ae388599568a78dcb029",
|
||||
"sha256:7b05ed4b35bf6ee790832f68932baf1f00caa32283d66cc4d455c9e9d115aafc",
|
||||
"sha256:7e109f1c9a3ece676597831874126555997c48f62bddbcace6ed17be3e372de8",
|
||||
"sha256:949844af60ee96a376aac1ded2a27e134b8c8d35cc006a52903fc06c24a3296f",
|
||||
"sha256:95304068686545aa368b35dfda1cdfbbdbe2f6fe43de4a2e9baa8ebd71be46e2",
|
||||
"sha256:9e662e6fc4f513b79da5d10a23edd2b87685815b337b1a30cd11307a6679148d",
|
||||
"sha256:a9fed35ca8c6e946e877893bbac022e8563b94404a605af1d1e6accc7eb73289",
|
||||
"sha256:b69522b168a6b64edf0c33ba53eac491c0a8f5cc94fa4337f9c6f4c8f2f5296c",
|
||||
"sha256:b78729038abea6a5df0d2708dce21e82073463b2d79d10884d7d591e0f385ded",
|
||||
"sha256:b8c56bec53d6e3154eaff6ea941226e7bd7cc0d99f9b3756c2520fc7a94e6d96",
|
||||
"sha256:b9727ac4f5cf2cbf87880a63870b5b9730a8ae3a4a360241a0fdaa2f71240ff0",
|
||||
"sha256:ba3027deb7abf02859aca49c865ece538aee56dcb4871b4cced23ba4d5088904",
|
||||
"sha256:be9fcf32c010da0ba40bf4ee01889d6c737658f4ddff160bd7eb9cac8f094b21",
|
||||
"sha256:c18d47f314b950dbf24a41787ced1474e01ca816011925976d90a88b27c22b89",
|
||||
"sha256:c76a3075e96b9c9ff00df8b5f7f560f5634dffd1658bafb79eb2682867e94f78",
|
||||
"sha256:cbfcba14a3225b055a28b3199c3d81cd0ab37d2353ffd7f6fd64844cebab31ad",
|
||||
"sha256:d254666d29540a72d17cc0175746cfb03d5123db33e67d1020e42dae611dc196",
|
||||
"sha256:d66187792bfe56f8c18ba986a0e4ae44856b1c645336bd2c776e3386da91e1dd",
|
||||
"sha256:d8d04e755934195bdc1db45ba9e040b8d20d046d04d6d77e71b3b34a8cc002d0",
|
||||
"sha256:d8f3e2e0a1d6777e58e834fd5a04657f66affa615dae61dd67c35d1568c38882",
|
||||
"sha256:e057e74e53db78122a3979f908973e171909a58ac20df05c33998d52e6d35757",
|
||||
"sha256:e4ce984133b888cc3a46867c8b4372c7dee9cee300335e2925e197bcd45b9e16",
|
||||
"sha256:ea76dbcad0b7b0deb265d8c36e0801abcddf6cc1395940a24e3595288b405ca0",
|
||||
"sha256:ecb0f73954892f98611e183f50acdc9e21a4653f294dfbe079da73c6378a6f47",
|
||||
"sha256:ef14d75d86f104f03dea66c13188487151760ef25dd6b2dbd541885185f05f40",
|
||||
"sha256:f26648e1b3b03b6022b48a9b910d0ae209e2d51f50441db5dce5b530fad6d9b1",
|
||||
"sha256:f67472c09a0c7486e27f3275f617c964d25e35727af952869dd496b9b5b7f6a3"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==7.0.2"
|
||||
"version": "==7.0.5"
|
||||
},
|
||||
"dill": {
|
||||
"hashes": [
|
||||
@@ -298,10 +320,11 @@
|
||||
},
|
||||
"iniconfig": {
|
||||
"hashes": [
|
||||
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
|
||||
"sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
|
||||
"sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
|
||||
"sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"isort": {
|
||||
"hashes": [
|
||||
@@ -313,28 +336,45 @@
|
||||
},
|
||||
"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"
|
||||
"sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382",
|
||||
"sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82",
|
||||
"sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9",
|
||||
"sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494",
|
||||
"sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46",
|
||||
"sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30",
|
||||
"sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63",
|
||||
"sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4",
|
||||
"sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae",
|
||||
"sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be",
|
||||
"sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701",
|
||||
"sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd",
|
||||
"sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006",
|
||||
"sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a",
|
||||
"sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586",
|
||||
"sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8",
|
||||
"sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821",
|
||||
"sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07",
|
||||
"sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b",
|
||||
"sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171",
|
||||
"sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b",
|
||||
"sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2",
|
||||
"sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7",
|
||||
"sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4",
|
||||
"sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8",
|
||||
"sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e",
|
||||
"sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f",
|
||||
"sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda",
|
||||
"sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4",
|
||||
"sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e",
|
||||
"sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671",
|
||||
"sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11",
|
||||
"sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455",
|
||||
"sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734",
|
||||
"sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb",
|
||||
"sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.8.0"
|
||||
"version": "==1.9.0"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
@@ -387,13 +427,21 @@
|
||||
],
|
||||
"version": "==0.4.3"
|
||||
},
|
||||
"mypy-protobuf": {
|
||||
"hashes": [
|
||||
"sha256:7d75a079651b105076776a35a5405e3fa773b8a167118f1b712e443e9a6c18a2",
|
||||
"sha256:da33dfde7547ff57e5ba5564126cbfa114f14413b2fa50759b1fa5de1e4ab511"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.4.0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3",
|
||||
"sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"
|
||||
"sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2",
|
||||
"sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==22.0"
|
||||
"version": "==23.0"
|
||||
},
|
||||
"platformdirs": {
|
||||
"hashes": [
|
||||
@@ -411,6 +459,26 @@
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.0.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"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053",
|
||||
@@ -429,19 +497,27 @@
|
||||
},
|
||||
"pylint": {
|
||||
"hashes": [
|
||||
"sha256:18783cca3cfee5b83c6c5d10b3cdb66c6594520ffae61890858fe8d932e1c6b4",
|
||||
"sha256:349c8cd36aede4d50a0754a8c0218b43323d13d5d88f4b2952ddfe3e169681eb"
|
||||
"sha256:9df0d07e8948a1c3ffa3b6e2d7e6e63d9fb457c5da5b961ed63106594780cc7e",
|
||||
"sha256:b3dc5ef7d33858f297ac0d06cc73862f01e4f2e74025ec3eff347ce0bc60baf5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.15.9"
|
||||
"version": "==2.15.10"
|
||||
},
|
||||
"pyproject-hooks": {
|
||||
"hashes": [
|
||||
"sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8",
|
||||
"sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71",
|
||||
"sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"
|
||||
"sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5",
|
||||
"sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.2.0"
|
||||
"version": "==7.2.1"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
|
||||
227
README.md
@@ -1,7 +1,7 @@
|
||||
# Extract secrets from QR codes exported by two-factor authentication apps
|
||||
# Extract TOTP/HOTP secrets from QR codes exported by two-factor authentication apps
|
||||
|
||||
[](https://github.com/scito/extract_otp_secrets/actions/workflows/ci.yml)
|
||||

|
||||

|
||||
[](https://github.com/scito/extract_otp_secrets/actions/workflows/ci_docker.yml)
|
||||

|
||||
[](https://github.com/scito/extract_otp_secrets/blob/master/Pipfile.lock)
|
||||
@@ -13,15 +13,61 @@
|
||||
---
|
||||
|
||||
The Python script `extract_otp_secrets.py` extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps such as "Google Authenticator".
|
||||
The export QR codes from authentication apps can be provided in three ways to this script:
|
||||
The exported QR codes from authentication apps can be read in three ways:
|
||||
|
||||
1. Capture from the system camera in a GUI,
|
||||
2. Image files containing the QR codes, and
|
||||
3. Text files containing the QR code data generated by QR readers.
|
||||
1. Capture the QR codes with the system camera using a GUI, 🆕
|
||||
2. Read image files containing the QR codes, and 🆕
|
||||
3. Read text files containing the QR code data generated by third-party QR readers.
|
||||
|
||||
The secret and otp values can be exported to json or csv files, as well as printed or saved to PNG images.
|
||||
The secrets can be exported to JSON or CSV, or printed as QR codes to console or saved as PNG.
|
||||
|
||||
This script/project was renamed from extract_otp_secret_keys to extract_otp_secrets in version 2.0.0.
|
||||
⚡ **This project/script was renamed from `extract_otp_secret_keys` to `extract_otp_secrets`.** ⚡
|
||||
|
||||
## Usage
|
||||
|
||||
### Capture QR codes from camera (🆕 since version 2.0)
|
||||
|
||||
1. Open "Google Authenticator" app on the mobile phone
|
||||
2. Export the QR codes from "Google Authenticator" app (see [how to export](#how-to-export-otp-secrets-from-google-authenticator-app))
|
||||
3. Point the exported QR codes to the camera of your computer
|
||||
4. Call this script without infile parameters:
|
||||
|
||||
python src/extract_otp_secrets.py
|
||||
|
||||

|
||||
|
||||
Detected QR codes are surrounded with a frame. The color of the frame indicates the extracting result:
|
||||
|
||||
* Green: The QR code is detected, decoded and the OTP secret was successfully extracted.
|
||||
* Red: The QR code is detected and decoded, but could not be successfully extracted. This is the case if a QR code not containing OTP data is captured.
|
||||
* Magenta: The QR code is detected, but could not be decoded. The QR code should be presented better to the camera or another QR reader could be used.
|
||||
|
||||
The secrets are printed by default to the console. [Set program parameters](#program-help-arguments-and-options) for other types of output, e.g. `--csv exported_secrets.csv`.
|
||||
|
||||
### With builtin QR decoder from image files (🆕 since version 2.0)
|
||||
|
||||
1. Open "Google Authenticator" app on the mobile phone
|
||||
2. Export the QR codes from "Google Authenticator" app (see [how to export](#how-to-export-otp-secrets-from-google-authenticator-app))
|
||||
4. Save the QR code as image file, e.g. example_export.png
|
||||
5. Transfer the images files to the computer where his script is installed.
|
||||
6. Call this script with the file as input:
|
||||
|
||||
python src/extract_otp_secrets.py example_export.png
|
||||
|
||||
7. Remove unencrypted files with secrets from your computer and mobile.
|
||||
|
||||
### With external QR decoder app from text files
|
||||
|
||||
1. Open "Google Authenticator" app on the mobile phone
|
||||
2. Export the QR codes from "Google Authenticator" app (see [how to export](#how-to-export-otp-secrets-from-google-authenticator-app))
|
||||
3. Read QR codes with a third-party QR code reader (e.g. from another phone)
|
||||
4. Save the captured QR codes from the QR code reader to a text file, e.g. example_export.txt. Save each QR code on a new line. (The captured QR codes look like `otpauth-migration://offline?data=…`)
|
||||
5. Transfer the file to the computer where his script is installed.
|
||||
6. Call this script with the file as input:
|
||||
|
||||
python src/extract_otp_secrets.py example_export.txt
|
||||
|
||||
7. Remove unencrypted files with secrets from your computer and mobile.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -42,7 +88,7 @@ If you do not use the `ZBAR` QR reader, you do not need to install the zbar shar
|
||||
|
||||
For a detailed installation documentation of [pyzbar](https://github.com/NaturalHistoryMuseum/pyzbar#installation).
|
||||
|
||||
#### Linux (Debian, Ubuntu, ...)
|
||||
#### Linux (Debian, Ubuntu, …)
|
||||
|
||||
sudo apt-get install libzbar0
|
||||
|
||||
@@ -54,6 +100,10 @@ For a detailed installation documentation of [pyzbar](https://github.com/Natural
|
||||
|
||||
sudo dnf install libzbar0
|
||||
|
||||
#### Linux (Arch Linux)
|
||||
|
||||
pacman -S zbar
|
||||
|
||||
#### Mac OS X
|
||||
|
||||
brew install zbar
|
||||
@@ -64,44 +114,10 @@ For a detailed installation documentation of [pyzbar](https://github.com/Natural
|
||||
|
||||
The zbar DLLs are included with the Windows Python wheels. However, you might need additionally to install [Visual C++ Redistributable Packages for Visual Studio 2013](https://www.microsoft.com/en-US/download/details.aspx?id=40784). Install `vcredist_x64.exe` if using 64-bit Python, `vcredist_x86.exe` if using 32-bit Python. For more information see [pyzbar](https://github.com/NaturalHistoryMuseum/pyzbar)
|
||||
|
||||
##### OpenCV
|
||||
##### OpenCV (CV2)
|
||||
|
||||
OpenCV requires [Visual C++ redistributable 2015](https://www.microsoft.com/en-us/download/details.aspx?id=48145). For more information see [opencv-python](https://pypi.org/project/opencv-python/)
|
||||
|
||||
## Usage
|
||||
|
||||
### Capture QR codes from camera (since version 2.0.0)
|
||||
|
||||
1. Open "Google Authenticator" app on the mobile phone
|
||||
2. Export the QR codes from "Google Authenticator" app
|
||||
3. Point the QR codes to the camera of your computer
|
||||
4. Call this script without infile parameters:
|
||||
|
||||
python src/extract_otp_secrets.py
|
||||
|
||||

|
||||
|
||||
### With builtin QR decoder from image files (since version 2.0.0)
|
||||
|
||||
1. Open "Google Authenticator" app on the mobile phone
|
||||
2. Export the QR codes from "Google Authenticator" app
|
||||
4. Save the QR code as image file, e.g. example_export.png
|
||||
5. Transfer the images files to the computer where his script is installed.
|
||||
6. Call this script with the file as input:
|
||||
|
||||
python src/extract_otp_secrets.py example_export.png
|
||||
|
||||
### With external QR decoder app from text files
|
||||
|
||||
1. Open "Google Authenticator" app on the mobile phone
|
||||
2. Export the QR codes from "Google Authenticator" app
|
||||
3. Read QR codes with a QR code reader (e.g. from another phone)
|
||||
4. Save the captured QR codes in the QR code reader to a text file, e.g. example_export.txt. Save each QR code on a new line. (The captured QR codes look like `otpauth-migration://offline?data=...`)
|
||||
5. Transfer the file to the computer where his script is installed.
|
||||
6. Call this script with the file as input:
|
||||
|
||||
python src/extract_otp_secrets.py example_export.txt
|
||||
|
||||
## Program help: arguments and options
|
||||
|
||||
<pre>usage: extract_otp_secrets.py [-h] [--csv FILE] [--keepass FILE] [--json FILE] [--printqr] [--saveqr DIR] [--camera NUMBER] [--qr {ZBAR,QREADER,QREADER_DEEP,CV2,CV2_WECHAT}] [-i] [--no-color] [-d | -v | -q] [infile ...]
|
||||
@@ -146,6 +162,14 @@ python extract_otp_secrets.py = < example_export.png</pre>
|
||||
|
||||
python src/extract_otp_secrets.py example_export.png
|
||||
|
||||
### Writing otp secrets to csv file
|
||||
|
||||
python src/extract_otp_secrets.py -q --csv extracted_secrets.csv example_export.txt
|
||||
|
||||
### Writing otp secrets to json file
|
||||
|
||||
python src/extract_otp_secrets.py -q --json extracted_secrets.json example_export.txt
|
||||
|
||||
### Printing otp secrets multiple files
|
||||
|
||||
python src/extract_otp_secrets.py example_*.txt
|
||||
@@ -177,13 +201,13 @@ python extract_otp_secrets.py = < example_export.png</pre>
|
||||
|
||||
* Free and open source
|
||||
* Supports Google Authenticator exports (and compatible apps like Aegis Authenticator)
|
||||
* Captures the the QR codes directly from the camera using different QR code libraries (based on OpenCV)
|
||||
* Captures the the QR codes directly from the camera using different QR code libraries (based on OpenCV) (🆕 since v2.0)
|
||||
* ZBAR: [pyzbar](https://github.com/NaturalHistoryMuseum/pyzbar) - fast and reliable, good for images and video capture (default and recommended)
|
||||
* QREADER: [QReader](https://github.com/Eric-Canas/QReader)
|
||||
* QREADER_DEEP: [QReader](https://github.com/Eric-Canas/QReader) - very slow in GUI
|
||||
* CV2: [QRCodeDetector](https://docs.opencv.org/4.x/de/dc3/classcv_1_1QRCodeDetector.html)
|
||||
* CV2_WECHAT: [WeChatQRCode](https://docs.opencv.org/4.x/dd/d63/group__wechat__qrcode.html)
|
||||
* Supports TOTP and HOTP standards
|
||||
* Supports [TOTP](https://www.ietf.org/rfc/rfc6238.txt) and [HOTP](https://www.ietf.org/rfc/rfc4226.txt) standards
|
||||
* Generates QR codes
|
||||
* Exports to various formats:
|
||||
* CSV
|
||||
@@ -191,7 +215,8 @@ python extract_otp_secrets.py = < example_export.png</pre>
|
||||
* Dedicated CSV for KeePass
|
||||
* QR code images
|
||||
* Supports reading from stdin and writing to stdout, thus pipes can be used
|
||||
* Reads QR codes images: (See [OpenCV docu](https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56))
|
||||
* Handles multiple input files (🆕 since v2.0)
|
||||
* Reads QR codes images: (See [OpenCV docu](https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56)) (🆕 since v2.0)
|
||||
* Portable Network Graphics - *.png
|
||||
* WebP - *.webp
|
||||
* JPEG files - *.jpeg, *.jpg, *.jpe
|
||||
@@ -199,8 +224,8 @@ python extract_otp_secrets.py = < example_export.png</pre>
|
||||
* Windows bitmaps - *.bmp, *.dib
|
||||
* JPEG 2000 files - *.jp2
|
||||
* Portable image format - *.pbm, *.pgm, *.ppm *.pxm, *.pnm
|
||||
* Prints errors and warnings to stderr
|
||||
* Prints colored output
|
||||
* Prints errors and warnings to stderr (🆕 since v2.0)
|
||||
* Prints colored output (🆕 since v2.0)
|
||||
* Many ways to run the script:
|
||||
* Native Python
|
||||
* pipenv
|
||||
@@ -209,7 +234,7 @@ python extract_otp_secrets.py = < example_export.png</pre>
|
||||
* Docker
|
||||
* VSCode devcontainer
|
||||
* devbox
|
||||
* Prebuilt Docker images provided for amd64 and arm64
|
||||
* Prebuilt Docker images provided for amd64 and arm64 (🆕 since v2.0)
|
||||
* Compatible with major platforms:
|
||||
* Linux
|
||||
* macOS
|
||||
@@ -259,23 +284,18 @@ Import CSV with HOTP entries in KeePass as
|
||||
|
||||
KeePass can be used as a backup for one time passwords (second factor) from the mobile phone.
|
||||
|
||||
## Technical background
|
||||
## How to export otp secrets from Google Authenticator app
|
||||
|
||||
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 or new protobuf versions):
|
||||
|
||||
protoc --plugin=protoc-gen-mypy=path/to/protoc-gen-mypy --python_out=src/protobuf_generated_python --mypy_out=src/protobuf_generated_python src/google_auth.proto
|
||||
|
||||
The generated protobuf Python code was generated by protoc 21.12 (https://github.com/protocolbuffers/protobuf/releases/tag/v21.12).
|
||||
|
||||
For Python type hint generation the [mypy-protobuf](https://github.com/nipunn1313/mypy-protobuf) package is used.
|
||||
|
||||
## References
|
||||
|
||||
* Proto3 documentation: https://developers.google.com/protocol-buffers/docs/pythontutorial
|
||||
* Template code: https://github.com/beemdevelopment/Aegis/pull/406
|
||||
1. Open "Google Authenticator" app
|
||||
2. Select "Transfer accounts" in the three dot menu of the app.
|
||||

|
||||
3. Select "Export accounts"
|
||||

|
||||
4. Pass the verification by password or fingerprint.
|
||||
5. Select your accounts
|
||||
6. Press "Next" button
|
||||
7. The exported QR code(s) ready for extraction are shown.
|
||||

|
||||
|
||||
## Glossary
|
||||
|
||||
@@ -308,7 +328,7 @@ curl -s https://raw.githubusercontent.com/scito/extract_otp_secrets/master/examp
|
||||
```
|
||||
git clone https://github.com/scito/extract_otp_secrets.git
|
||||
pip install -U -e extract_otp_secrets
|
||||
python -m extract_otp_secrets example_export.txt
|
||||
python -m extract_otp_secrets extract_otp_secrets/example_export.txt
|
||||
```
|
||||
|
||||
### pipenv
|
||||
@@ -364,11 +384,10 @@ Prebuilt docker images are available for amd64 and arm64 architectures on [Docke
|
||||
Extracting from an QR image file:
|
||||
|
||||
```
|
||||
docker login -u USERNAME
|
||||
curl -s https://raw.githubusercontent.com/scito/extract_otp_secrets/master/example_export.png | docker run --pull always -i --rm -v "$(pwd)":/files:ro scit0/extract_otp_secrets =
|
||||
```
|
||||
|
||||
Capturing from camera in GUI (X Window system required on host):
|
||||
Capturing from camera in GUI window (X Window system required on host):
|
||||
|
||||
```
|
||||
docker run --pull always --rm -v "$(pwd)":/files:ro -i --device="/dev/video0:/dev/video0" --env="DISPLAY" -v /tmp/.X11-unix:/tmp/.X11-unix:ro scit0/extract_otp_secrets
|
||||
@@ -447,6 +466,7 @@ Setup for running the tests in VSCode.
|
||||
### Build
|
||||
|
||||
```
|
||||
cd extract_otp_secrets/
|
||||
pip install -U -e .
|
||||
python src/extract_otp_secrets.py
|
||||
|
||||
@@ -463,29 +483,79 @@ pip install -U -r requirements.txt
|
||||
|
||||
### Build docker images
|
||||
|
||||
#### Debian (full functionality)
|
||||
|
||||
Build and run the app within the container:
|
||||
|
||||
```bash
|
||||
docker build . -t extract_otp_secrets --pull --build-arg RUN_TESTS=false
|
||||
```
|
||||
|
||||
```bash
|
||||
docker build . -t extract_otp_secrets_only_txt --pull -f Dockerfile_only_txt --build-arg RUN_TESTS=false
|
||||
```
|
||||
|
||||
Run tests in docker container:
|
||||
|
||||
```bash
|
||||
docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extract_otp_secrets
|
||||
```
|
||||
|
||||
#### Alpine (only text file processing)
|
||||
|
||||
```bash
|
||||
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
|
||||
docker build . -t extract_otp_secrets_only_txt --pull -f Dockerfile_only_txt --build-arg RUN_TESTS=false
|
||||
```
|
||||
|
||||
Run tests in docker container:
|
||||
|
||||
```bash
|
||||
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" --relaxed
|
||||
```
|
||||
|
||||
### Full local build
|
||||
|
||||
There is a Bash script for a full local build including linting and type checking.
|
||||
|
||||
```bash
|
||||
./build.sh
|
||||
```
|
||||
|
||||
The options of the build script:
|
||||
|
||||
```
|
||||
Build extract_otp_secrets project
|
||||
|
||||
./build.sh [options]
|
||||
|
||||
Options:
|
||||
-i Interactive mode, all steps must be confirmed
|
||||
-C Ignore version check of protobuf/protoc
|
||||
-D Do not build docker
|
||||
-G Do not start extract_otp_secrets.py in GUI mode
|
||||
-c Clean everything
|
||||
-r Generate result files
|
||||
-h, --help Help
|
||||
```
|
||||
|
||||
## 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 or new protobuf versions):
|
||||
|
||||
protoc --plugin=protoc-gen-mypy=path/to/protoc-gen-mypy --python_out=src/protobuf_generated_python --mypy_out=src/protobuf_generated_python src/google_auth.proto
|
||||
|
||||
The generated protobuf Python code was generated by protoc 21.12 (https://github.com/protocolbuffers/protobuf/releases/tag/v21.12).
|
||||
|
||||
For Python type hint generation the [mypy-protobuf](https://github.com/nipunn1313/mypy-protobuf) package is used.
|
||||
|
||||
## References
|
||||
|
||||
* Proto3 documentation: https://developers.google.com/protocol-buffers/docs/pythontutorial
|
||||
* Template code: https://github.com/beemdevelopment/Aegis/pull/406
|
||||
|
||||
## Issues
|
||||
|
||||
* Known issue for macOS: https://github.com/opencv/opencv/issues/23072
|
||||
* Segmentation fault on macOS with CV2 4.7.0: https://github.com/opencv/opencv/issues/23072
|
||||
* CV2 window does not show icons: https://github.com/opencv/opencv-python/issues/585
|
||||
|
||||
## Problems and Troubleshooting
|
||||
|
||||
@@ -518,10 +588,11 @@ FileNotFoundError: Could not find module 'libiconv.dll' (or one of its dependenc
|
||||
|
||||
* [ZBar](https://github.com/mchehab/zbar) is an open source software suite for reading bar codes from various sources, including webcams.
|
||||
* [Aegis Authenticator](https://github.com/beemdevelopment/Aegis) is a free, secure and open source 2FA app for Android.
|
||||
* [Android OTP Extractor](https://github.com/puddly/android-otp-extractor) can extract your tokens from popular Android OTP apps and export them in a standard format or just display them as QR codes for easy importing. [Requires a _rooted_ Android phone.]
|
||||
* [Python QReader](https://github.com/Eric-Canas/QReader)
|
||||
* [pyzbar](https://github.com/NaturalHistoryMuseum/pyzbar)
|
||||
* [pyzbar](https://github.com/NaturalHistoryMuseum/pyzbar) is a good QR code reader Python module
|
||||
* [OpenCV](https://docs.opencv.org/4.x/) (CV2) Open Source Computer Vision library with [opencv-python](https://github.com/opencv/opencv-python)
|
||||
* [Python QReader](https://github.com/Eric-Canas/QReader) Python QR code readers
|
||||
* [Android OTP Extractor](https://github.com/puddly/android-otp-extractor) can extract your tokens from popular Android OTP apps and export them in a standard format or just display them as QR codes for easy importing. [Requires a _rooted_ Android phone.]
|
||||
* [Google Authenticator secret extractor](https://github.com/krissrex/google-authenticator-exporter) is similar project written in JavaScript. It also extracts otp secrets from Google Authenticator.
|
||||
|
||||
***
|
||||
|
||||
|
||||
116
build.sh
@@ -73,11 +73,6 @@ askContinueYn() {
|
||||
|
||||
# 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=false
|
||||
ignore_version_check=true
|
||||
clean=false
|
||||
@@ -88,16 +83,16 @@ generate_result_files=false
|
||||
while test $# -gt 0; do
|
||||
case $1 in
|
||||
-h|--help)
|
||||
echo "Upgrade Protoc"
|
||||
echo "Build extract_otp_secrets project"
|
||||
echo
|
||||
echo "$0 [options]"
|
||||
echo
|
||||
echo "Options:"
|
||||
echo "-i Interactive"
|
||||
echo "-C Ignore version check"
|
||||
echo "-D No docker build"
|
||||
echo "-G No not run gui"
|
||||
echo "-c Clean"
|
||||
echo "-i Interactive mode, all steps must be confirmed"
|
||||
echo "-C Ignore version check of protobuf/protoc"
|
||||
echo "-D Do not build docker"
|
||||
echo "-G Do not start extract_otp_secrets.py in GUI mode"
|
||||
echo "-c Clean everything"
|
||||
echo "-r Generate result files"
|
||||
echo "-h, --help Help"
|
||||
quit
|
||||
@@ -138,15 +133,33 @@ PIPENV="$PYTHON -m pipenv"
|
||||
FLAKE8="$PYTHON -m flake8"
|
||||
MYPY="$PYTHON -m mypy"
|
||||
|
||||
# sudo ln -s /usr/bin/python3.11 /usr/bin/python
|
||||
|
||||
# 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="docker image prune -f || echo 'No docker image pruned'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIP uninstall -y extract-otp-secrets || echo nothing done"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIP freeze | grep -v -E '^-e|^#' | xargs sudo $PIP uninstall -y || echo nothing done"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIP freeze --user | grep -v -E '^-e|^#' | xargs $PIP uninstall -y || echo nothing done"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIP freeze | cut -d \"@\" -f1 | xargs pip uninstall -y || echo Nothing to do"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
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; mkdir -p tests/reports;"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
@@ -160,6 +173,20 @@ if $clean; then
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
cmd="$PIP install --use-pep517 -U -r requirements-dev.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
|
||||
echo -e "\n\nChecking 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
|
||||
|
||||
OLDVERSION=$(cat $BIN/$DEST/.VERSION.txt || echo "")
|
||||
echo -e "\nProtoc remote version $VERSION\n"
|
||||
echo -e "Protoc local version: $OLDVERSION\n"
|
||||
|
||||
if [ "$OLDVERSION" != "$VERSION" ] || ! $ignore_version_check; then
|
||||
echo "Upgrade protoc from $OLDVERSION to $VERSION"
|
||||
|
||||
@@ -216,7 +243,7 @@ fi
|
||||
|
||||
# Upgrade pip requirements
|
||||
|
||||
cmd="sudo pip install -U pip"
|
||||
cmd="pip install -U pip"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
@@ -226,10 +253,6 @@ 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"
|
||||
|
||||
# Lint
|
||||
|
||||
LINT_OUT_FILE="tests/reports/flake8_results.txt"
|
||||
@@ -254,7 +277,21 @@ cmd="$MYPY --strict src/*.py tests/*.py | tee $TYPE_CHECK_OUT_FILE"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Test
|
||||
# pip -e install
|
||||
|
||||
cmd="$PIP install -U -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 (needs module)
|
||||
|
||||
cmd="$PYTHON src/extract_otp_secrets.py example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
@@ -287,37 +324,9 @@ cmd="$PIPENV run pytest --cov=extract_otp_secrets_test tests/"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# sudo pip
|
||||
|
||||
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"
|
||||
|
||||
# pip -e install (must be after other pip installs in order to have this environment for development)
|
||||
|
||||
cmd="$PIP install -U -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"
|
||||
|
||||
# Build wheel
|
||||
|
||||
cmd="$PIP wheel .
|
||||
cmd="$PIP wheel ."
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
@@ -378,10 +387,6 @@ if $build_docker; then
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker image prune -f || echo 'No docker image pruned'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
if $run_gui; then
|
||||
cmd="docker run --rm -v "$(pwd)":/files:ro --device=\"/dev/video0:/dev/video0\" --env=\"DISPLAY\" -v /tmp/.X11-unix:/tmp/.X11-unix:ro extract_otp_secrets &"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
@@ -402,6 +407,7 @@ cmd="cat $TYPE_CHECK_OUT_FILE $LINT_OUT_FILE $COVERAGE_OUT_FILE"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
echo -e "\n${greenBold}SUCCESS${reset}"
|
||||
line=$(printf '#%.0s' $(eval echo {1..$(( ($COLUMNS - 10) / 2))}))
|
||||
echo -e "\n${greenBold}$line SUCCESS $line${reset}"
|
||||
|
||||
quit
|
||||
|
||||
BIN
docs/Export-account-option-in-the-Google-Authenticator.webp
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
BIN
docs/Exported-QR-codes.webp
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
docs/Exported-QR-codes_300px.webp
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
docs/Transfer-accounts-option-in-the-Google-Authenticator.webp
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
@@ -16,6 +16,8 @@ classifiers = [
|
||||
"Environment :: Win32 (MS Windows)",
|
||||
"Topic :: System :: Archiving :: Backup",
|
||||
"Topic :: Utilities",
|
||||
"Topic :: Security",
|
||||
"Topic :: Multimedia :: Graphics :: Capture :: Digital Camera",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
@@ -27,17 +29,18 @@ classifiers = [
|
||||
"Programming Language :: Python",
|
||||
"Natural Language :: English",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Typing :: Typed",
|
||||
]
|
||||
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",
|
||||
"opencv-contrib-python; sys_platform != 'darwin'",
|
||||
"opencv-contrib-python<=4.7.0; sys_platform == 'darwin'",
|
||||
"Pillow",
|
||||
"protobuf",
|
||||
"pyzbar",
|
||||
"qrcode",
|
||||
"qreader<2.0.0",
|
||||
"typing_extensions; python_version<='3.7'",
|
||||
]
|
||||
description = "Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps such as 'Google Authenticator'"
|
||||
dynamic = ["version"]
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
build
|
||||
flake8
|
||||
mypy
|
||||
types-protobuf
|
||||
mypy-protobuf
|
||||
pylint
|
||||
pytest
|
||||
pytest-mock
|
||||
pytest-cov
|
||||
pytest-mock
|
||||
setuptools
|
||||
types-protobuf
|
||||
wheel
|
||||
build
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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
|
||||
opencv-contrib-python; sys_platform != 'darwin'
|
||||
opencv-contrib-python<=4.7.0; sys_platform == 'darwin'
|
||||
Pillow
|
||||
protobuf
|
||||
pyzbar
|
||||
qrcode
|
||||
qreader<2.0.0
|
||||
typing_extensions; python_version<='3.7'
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# Source code available on https://github.com/scito/extract_otp_secrets
|
||||
#
|
||||
# Technical background:
|
||||
# The export QR code from "Google Authenticator" contains the URL "otpauth-migration://offline?data=...".
|
||||
# The export QR code from "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):
|
||||
@@ -164,6 +164,77 @@ def main(sys_args: list[str]) -> None:
|
||||
write_json(args, otps)
|
||||
|
||||
|
||||
# workaround for PYTHON <= 3.9 use: pb.MigrationPayload | None
|
||||
def get_payload_from_otp_url(otp_url: str, i: int, source: str) -> Optional[pb.MigrationPayload]:
|
||||
'''Extracts the otp migration payload from an otp url. This function is the core of the this appliation.'''
|
||||
if not is_opt_url(otp_url, source):
|
||||
return None
|
||||
parsed_url = urlparse.urlparse(otp_url)
|
||||
if verbose >= LogLevel.DEBUG: log_debug(f"parsed_url={parsed_url}")
|
||||
try:
|
||||
params = urlparse.parse_qs(parsed_url.query, strict_parsing=True)
|
||||
except Exception: # workaround for PYTHON <= 3.10
|
||||
params = {}
|
||||
if verbose >= LogLevel.DEBUG: log_debug(f"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 >= LogLevel.DEBUG: log_debug(f"data_base64={data_base64}")
|
||||
data_base64_fixed = data_base64.replace(' ', '+')
|
||||
if verbose >= LogLevel.DEBUG: log_debug(f"data_base64_fixed={data_base64_fixed}")
|
||||
data = base64.b64decode(data_base64_fixed, validate=True)
|
||||
payload = pb.MigrationPayload()
|
||||
try:
|
||||
payload.ParseFromString(data)
|
||||
except Exception as e:
|
||||
abort(f"Cannot decode otpauth-migration migration payload.\n"
|
||||
f"data={data_base64}", e)
|
||||
if verbose >= LogLevel.DEBUG: log_debug(f"\n{i}. Payload Line", payload, sep='\n')
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, urls_count: int, infile: str, args: Args) -> int:
|
||||
'''Converts the otp migration payload into a normal Python dictionary. This function is the core of the this appliation.'''
|
||||
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:
|
||||
if verbose: print(f"\n{len(otps) + 1}. Secret")
|
||||
secret = convert_secret_from_bytes_to_base32_str(raw_otp.secret)
|
||||
if verbose >= LogLevel.DEBUG: log_debug('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
|
||||
}
|
||||
if otp not in otps or not args.ignore:
|
||||
otps.append(otp)
|
||||
new_otps_count += 1
|
||||
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()
|
||||
elif args.ignore and not quiet:
|
||||
eprint(f"Ignored duplicate otp: {otp['name']}", f" / {otp['issuer']}\n" if otp['issuer'] else '\n', sep='')
|
||||
|
||||
return new_otps_count
|
||||
|
||||
|
||||
def parse_args(sys_args: list[str]) -> Args:
|
||||
global verbose, quiet, colored
|
||||
description_text = "Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps"
|
||||
@@ -220,42 +291,6 @@ def extract_otps(args: Args) -> Otps:
|
||||
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
|
||||
|
||||
|
||||
# TODO use cv2 types if available
|
||||
def cv2_draw_box(img: Any, raw_pts: Any, color: ColorBGR) -> Any:
|
||||
pts = np.array([raw_pts], np.int32)
|
||||
pts = pts.reshape((-1, 1, 2))
|
||||
cv2.polylines(img, [pts], True, color, BOX_THICKNESS)
|
||||
return pts
|
||||
|
||||
|
||||
# TODO use cv2 types if available
|
||||
def cv2_print_text(img: Any, text: str, line_number: int, position: TextPosition, color: ColorBGR, opposite_len: Optional[int] = None) -> None:
|
||||
window_dim = cv2.getWindowImageRect(WINDOW_NAME)
|
||||
out_text = text
|
||||
if opposite_len:
|
||||
text_dim, _ = cv2.getTextSize(out_text, FONT, FONT_SCALE, FONT_THICKNESS)
|
||||
actual_width = text_dim[TEXT_WIDTH] + opposite_len * CHAR_DX + 4 * BORDER
|
||||
if actual_width >= window_dim[WINDOW_WIDTH]:
|
||||
out_text = out_text[:(window_dim[WINDOW_WIDTH] - actual_width) // CHAR_DX] + '.'
|
||||
text_dim, _ = cv2.getTextSize(out_text, FONT, FONT_SCALE, FONT_THICKNESS)
|
||||
if position == TextPosition.LEFT:
|
||||
pos = BORDER, START_Y + line_number * FONT_DY
|
||||
else:
|
||||
pos = window_dim[WINDOW_WIDTH] - text_dim[TEXT_WIDTH] - BORDER, START_Y + line_number * FONT_DY
|
||||
|
||||
cv2.putText(img, out_text, pos, FONT, FONT_SCALE, color, FONT_THICKNESS, FONT_LINE_STYLE)
|
||||
|
||||
|
||||
def extract_otps_from_camera(args: Args) -> Otps:
|
||||
if verbose: print("Capture QR codes from camera")
|
||||
otp_urls: OtpUrls = []
|
||||
@@ -285,9 +320,9 @@ def extract_otps_from_camera(args: Args) -> Otps:
|
||||
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), BOX_THICKNESS)
|
||||
cv2_draw_box(img, [(bbox[0], bbox[1]), (bbox[2], bbox[1]), (bbox[2], bbox[3]), (bbox[0], bbox[3])], get_color(new_otps_count, otp_url))
|
||||
elif qr_mode == QRMode.ZBAR:
|
||||
for qrcode in zbar.decode(img):
|
||||
for qrcode in zbar.decode(img, symbols=[zbar.ZBarSymbol.QRCODE]):
|
||||
otp_url = qrcode.data.decode('utf-8')
|
||||
new_otps_count = extract_otps_from_otp_url(otp_url, otp_urls, otps, args)
|
||||
cv2_draw_box(img, [qrcode.polygon], get_color(new_otps_count, otp_url))
|
||||
@@ -325,6 +360,42 @@ def extract_otps_from_camera(args: Args) -> Otps:
|
||||
return otps
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
# TODO use cv2 types if available
|
||||
def cv2_draw_box(img: Any, raw_pts: Any, color: ColorBGR) -> Any:
|
||||
pts = np.array([raw_pts], np.int32)
|
||||
pts = pts.reshape((-1, 1, 2))
|
||||
cv2.polylines(img, [pts], True, color, BOX_THICKNESS)
|
||||
return pts
|
||||
|
||||
|
||||
# TODO use cv2 types if available
|
||||
def cv2_print_text(img: Any, text: str, line_number: int, position: TextPosition, color: ColorBGR, opposite_len: Optional[int] = None) -> None:
|
||||
window_dim = cv2.getWindowImageRect(WINDOW_NAME)
|
||||
out_text = text
|
||||
if opposite_len:
|
||||
text_dim, _ = cv2.getTextSize(out_text, FONT, FONT_SCALE, FONT_THICKNESS)
|
||||
actual_width = text_dim[TEXT_WIDTH] + opposite_len * CHAR_DX + 4 * BORDER
|
||||
if actual_width >= window_dim[WINDOW_WIDTH]:
|
||||
out_text = out_text[:(window_dim[WINDOW_WIDTH] - actual_width) // CHAR_DX] + '.'
|
||||
text_dim, _ = cv2.getTextSize(out_text, FONT, FONT_SCALE, FONT_THICKNESS)
|
||||
if position == TextPosition.LEFT:
|
||||
pos = BORDER, START_Y + line_number * FONT_DY
|
||||
else:
|
||||
pos = window_dim[WINDOW_WIDTH] - text_dim[TEXT_WIDTH] - BORDER, START_Y + line_number * FONT_DY
|
||||
|
||||
cv2.putText(img, out_text, pos, FONT, FONT_SCALE, color, FONT_THICKNESS, FONT_LINE_STYLE)
|
||||
|
||||
|
||||
def cv2_handle_pressed_keys(qr_mode: QRMode) -> Tuple[bool, QRMode]:
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
quit = False
|
||||
@@ -414,45 +485,6 @@ def read_lines_from_text_file(filename: str) -> list[str]:
|
||||
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:
|
||||
if verbose: print(f"\n{len(otps) + 1}. Secret")
|
||||
secret = convert_secret_from_bytes_to_base32_str(raw_otp.secret)
|
||||
if verbose >= LogLevel.DEBUG: log_debug('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
|
||||
}
|
||||
if otp not in otps or not args.ignore:
|
||||
otps.append(otp)
|
||||
new_otps_count += 1
|
||||
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()
|
||||
elif args.ignore and not quiet:
|
||||
eprint(f"Ignored duplicate otp: {otp['name']}", f" / {otp['issuer']}\n" if otp['issuer'] else '\n', sep='')
|
||||
|
||||
return new_otps_count
|
||||
|
||||
|
||||
def convert_img_to_otp_urls(filename: str, args: Args) -> OtpUrls:
|
||||
if verbose: print(f"Reading image {filename}")
|
||||
try:
|
||||
@@ -498,7 +530,7 @@ def decode_qr_img_otp_urls(img: Any, qr_mode: QRMode) -> OtpUrls:
|
||||
otp_url, _ = cv2.wechat_qrcode.WeChatQRCode().detectAndDecode(img)
|
||||
otp_urls += list(otp_url)
|
||||
elif qr_mode == QRMode.ZBAR:
|
||||
qrcodes = zbar.decode(img)
|
||||
qrcodes = zbar.decode(img, symbols=[zbar.ZBarSymbol.QRCODE])
|
||||
otp_urls += [qrcode.data.decode('utf-8') for qrcode in qrcodes]
|
||||
else:
|
||||
assert False, f"Wrong QReader mode {qr_mode.name}"
|
||||
@@ -506,37 +538,6 @@ def decode_qr_img_otp_urls(img: Any, qr_mode: QRMode) -> OtpUrls:
|
||||
return otp_urls
|
||||
|
||||
|
||||
# workaround for PYTHON <= 3.9 use: pb.MigrationPayload | None
|
||||
def get_payload_from_otp_url(otp_url: str, i: int, source: str) -> Optional[pb.MigrationPayload]:
|
||||
'''Extracts the otp migration payload from an otp url. This function is the core of the this appliation.'''
|
||||
if not is_opt_url(otp_url, source):
|
||||
return None
|
||||
parsed_url = urlparse.urlparse(otp_url)
|
||||
if verbose >= LogLevel.DEBUG: log_debug(f"parsed_url={parsed_url}")
|
||||
try:
|
||||
params = urlparse.parse_qs(parsed_url.query, strict_parsing=True)
|
||||
except Exception: # workaround for PYTHON <= 3.10
|
||||
params = {}
|
||||
if verbose >= LogLevel.DEBUG: log_debug(f"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 >= LogLevel.DEBUG: log_debug(f"data_base64={data_base64}")
|
||||
data_base64_fixed = data_base64.replace(' ', '+')
|
||||
if verbose >= LogLevel.DEBUG: log_debug(f"data_base64_fixed={data_base64_fixed}")
|
||||
data = base64.b64decode(data_base64_fixed, validate=True)
|
||||
payload = pb.MigrationPayload()
|
||||
try:
|
||||
payload.ParseFromString(data)
|
||||
except Exception as e:
|
||||
abort(f"Cannot decode otpauth-migration migration payload.\n"
|
||||
f"data={data_base64}", e)
|
||||
if verbose >= LogLevel.DEBUG: log_debug(f"\n{i}. Payload Line", payload, sep='\n')
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def is_opt_url(otp_url: str, source: str) -> bool:
|
||||
if not otp_url.startswith('otpauth-migration://'):
|
||||
msg = f"input is not a otpauth-migration:// url\nsource: {source}\ninput: {otp_url}"
|
||||
|
||||
@@ -2,8 +2,6 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from extract_otp_secrets import QRMode
|
||||
|
||||
|
||||
def pytest_addoption(parser: pytest.Parser) -> None:
|
||||
parser.addoption("--relaxed", action='store_true', help="run tests in relaxed mode")
|
||||
@@ -17,6 +15,7 @@ def relaxed(request: pytest.FixtureRequest) -> Any:
|
||||
|
||||
def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
|
||||
if "qr_mode" in metafunc.fixturenames:
|
||||
number = 2 if metafunc.config.getoption("fast") else len(QRMode)
|
||||
qr_modes = [mode.name for mode in QRMode]
|
||||
all_qr_modes = ['ZBAR', 'QREADER', 'QREADER_DEEP', 'CV2', 'CV2_WECHAT']
|
||||
number = 2 if metafunc.config.getoption("fast") else len(all_qr_modes)
|
||||
qr_modes = [mode for mode in all_qr_modes]
|
||||
metafunc.parametrize("qr_mode", qr_modes[0:number])
|
||||
|
||||
3
tests/data/only_comments.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# comment 1
|
||||
|
||||
# comment 2
|
||||
BIN
tests/data/qr_but_without_otp.png
Normal file
|
After Width: | Height: | Size: 478 B |
@@ -26,7 +26,8 @@ import pathlib
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
import colorama
|
||||
import pytest
|
||||
@@ -37,6 +38,13 @@ from utils import (count_files_in_dir, file_exits, read_binary_file_as_stream,
|
||||
|
||||
import extract_otp_secrets
|
||||
|
||||
try:
|
||||
import cv2 # type: ignore
|
||||
from extract_otp_secrets import SUCCESS_COLOR, FAILURE_COLOR, FONT, FONT_SCALE, FONT_COLOR, FONT_THICKNESS, FONT_LINE_STYLE
|
||||
except ImportError:
|
||||
# ignore
|
||||
pass
|
||||
|
||||
qreader_available: bool = extract_otp_secrets.qreader_available
|
||||
|
||||
|
||||
@@ -99,6 +107,20 @@ def test_extract_stdin_empty(capsys: pytest.CaptureFixture[str], monkeypatch: py
|
||||
assert captured.err == '\nWARN: stdin is empty\n'
|
||||
|
||||
|
||||
def test_extract_stdin_only_comments(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Arrange
|
||||
monkeypatch.setattr('sys.stdin', io.StringIO("\n\n# comment 1\n\n\n#comment 2"))
|
||||
|
||||
# Act
|
||||
extract_otp_secrets.main(['-n', '-'])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out == ''
|
||||
assert captured.err == ''
|
||||
|
||||
|
||||
def test_extract_empty_file_no_qreader(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
if qreader_available:
|
||||
# Act
|
||||
@@ -125,6 +147,17 @@ def test_extract_empty_file_no_qreader(capsys: pytest.CaptureFixture[str]) -> No
|
||||
assert captured.out == ''
|
||||
|
||||
|
||||
def test_extract_only_comments_file_no_qreader(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
# Act
|
||||
extract_otp_secrets.main(['-n', 'tests/data/only_comments.txt'])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.err == ''
|
||||
assert captured.out == ''
|
||||
|
||||
|
||||
@pytest.mark.qreader
|
||||
def test_extract_stdin_img_empty(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Arrange
|
||||
@@ -140,6 +173,24 @@ def test_extract_stdin_img_empty(capsys: pytest.CaptureFixture[str], monkeypatch
|
||||
assert captured.err == '\nWARN: stdin is empty\n'
|
||||
|
||||
|
||||
@pytest.mark.qreader
|
||||
def test_extract_stdin_img_garbage(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Arrange
|
||||
monkeypatch.setattr('sys.stdin', io.BytesIO("garbage".encode('utf-8')))
|
||||
|
||||
# Act
|
||||
with pytest.raises(SystemExit) as e:
|
||||
extract_otp_secrets.main(['-n', '='])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out == ''
|
||||
assert captured.err == '\nERROR: Unable to open file for reading.\ninput file: =\n'
|
||||
assert e.type == SystemExit
|
||||
assert e.value.code == 1
|
||||
|
||||
|
||||
def test_extract_csv(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path) -> None:
|
||||
# Arrange
|
||||
output_file = str(tmp_path / 'test_example_output.csv')
|
||||
@@ -175,6 +226,19 @@ def test_extract_csv_stdout(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
assert captured.err == ''
|
||||
|
||||
|
||||
def test_extract_csv_stdout_only_comments(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
# Act
|
||||
extract_otp_secrets.main(['-c', '-', 'tests/data/only_comments.txt'])
|
||||
|
||||
# Assert
|
||||
assert not file_exits('test_example_output.csv')
|
||||
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out == ''
|
||||
assert captured.err == ''
|
||||
|
||||
|
||||
def test_extract_stdin_and_csv_stdout(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Arrange
|
||||
monkeypatch.setattr('sys.stdin', io.StringIO(read_file_to_str('example_export.txt')))
|
||||
@@ -218,6 +282,18 @@ def test_keepass_csv(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path)
|
||||
assert captured.err == ''
|
||||
|
||||
|
||||
def test_keepass_empty(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path) -> None:
|
||||
# Act
|
||||
extract_otp_secrets.main(['-k', '-', 'tests/data/only_comments.txt'])
|
||||
|
||||
# Assert
|
||||
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out == ''
|
||||
assert captured.err == ''
|
||||
|
||||
|
||||
def test_keepass_csv_stdout(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
'''Two csv files .totp and .htop are generated.'''
|
||||
# Act
|
||||
@@ -288,6 +364,18 @@ def test_extract_json_stdout(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
assert captured.err == ''
|
||||
|
||||
|
||||
def test_extract_json_stdout_only_comments(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
# Act
|
||||
extract_otp_secrets.main(['-j', '-', 'tests/data/only_comments.txt'])
|
||||
|
||||
# Assert
|
||||
assert not file_exits('test_example_output.json')
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out == '[]'
|
||||
assert captured.err == ''
|
||||
|
||||
|
||||
def test_extract_not_encoded_plus(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
# Act
|
||||
extract_otp_secrets.main(['tests/data/test_plus_problem_export.txt'])
|
||||
@@ -494,6 +582,113 @@ def test_extract_no_arguments(capsys: pytest.CaptureFixture[str], mocker: Mocker
|
||||
assert e.type == SystemExit
|
||||
|
||||
|
||||
MockMode = Enum('MockMode', ['REPEAT_FIRST_ENDLESS', 'LOOP_LIST'])
|
||||
|
||||
|
||||
class MockCam:
|
||||
|
||||
read_counter: int = 0
|
||||
read_files: List[str] = []
|
||||
mock_mode: MockMode
|
||||
|
||||
def __init__(self, files: List[str] = ['example_export.png'], mock_mode: MockMode = MockMode.REPEAT_FIRST_ENDLESS):
|
||||
self.read_files = files
|
||||
self.image_mode = mock_mode
|
||||
|
||||
def read(self) -> Tuple[bool, Any]:
|
||||
if self.image_mode == MockMode.REPEAT_FIRST_ENDLESS:
|
||||
file = self.read_files[0]
|
||||
elif self.image_mode == MockMode.LOOP_LIST:
|
||||
file = self.read_files[self.read_counter]
|
||||
self.read_counter += 1
|
||||
|
||||
if file:
|
||||
img = cv2.imread(file)
|
||||
return True, img
|
||||
else:
|
||||
return False, None
|
||||
|
||||
def release(self) -> None:
|
||||
# ignore
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize("qr_reader,file,success", [
|
||||
(None, 'example_export.png', True),
|
||||
('ZBAR', 'example_export.png', True),
|
||||
('QREADER', 'example_export.png', True),
|
||||
('QREADER_DEEP', 'example_export.png', True),
|
||||
('CV2', 'example_export.png', True),
|
||||
('CV2_WECHAT', 'example_export.png', True),
|
||||
(None, 'tests/data/qr_but_without_otp.png', False),
|
||||
('ZBAR', 'tests/data/qr_but_without_otp.png', False),
|
||||
('QREADER', 'tests/data/qr_but_without_otp.png', False),
|
||||
('QREADER_DEEP', 'tests/data/qr_but_without_otp.png', False),
|
||||
('CV2', 'tests/data/qr_but_without_otp.png', False),
|
||||
('CV2_WECHAT', 'tests/data/qr_but_without_otp.png', False),
|
||||
(None, 'tests/data/lena_std.tif', None),
|
||||
('ZBAR', 'tests/data/lena_std.tif', None),
|
||||
('QREADER', 'tests/data/lena_std.tif', None),
|
||||
('QREADER_DEEP', 'tests/data/lena_std.tif', None),
|
||||
('CV2', 'tests/data/lena_std.tif', None),
|
||||
('CV2_WECHAT', 'tests/data/lena_std.tif', None),
|
||||
])
|
||||
def test_extract_otps_from_camera(qr_reader: Optional[str], file: str, success: bool, capsys: pytest.CaptureFixture[str], mocker: MockerFixture) -> None:
|
||||
if qreader_available:
|
||||
# Arrange
|
||||
mockCam = MockCam([file])
|
||||
mocker.patch('cv2.VideoCapture', return_value=mockCam)
|
||||
mocker.patch('cv2.namedWindow')
|
||||
mocked_polylines = mocker.patch('cv2.polylines')
|
||||
mocker.patch('cv2.imshow')
|
||||
mocker.patch('cv2.getTextSize', return_value=([8, 200], False))
|
||||
mocked_putText = mocker.patch('cv2.putText')
|
||||
mocker.patch('cv2.getWindowImageRect', return_value=[0, 0, 640, 480])
|
||||
mocker.patch('cv2.waitKey', return_value=27)
|
||||
mocker.patch('cv2.getWindowProperty', return_value=False)
|
||||
mocker.patch('cv2.destroyAllWindows')
|
||||
|
||||
args = []
|
||||
if qr_reader:
|
||||
args.append('-Q')
|
||||
args.append(qr_reader)
|
||||
# Act
|
||||
extract_otp_secrets.main(args)
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
if success:
|
||||
assert captured.out == EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT_PNG
|
||||
assert captured.err == ''
|
||||
mocked_polylines.assert_called_with(mocker.ANY, mocker.ANY, True, SUCCESS_COLOR, mocker.ANY)
|
||||
mocked_putText.assert_called_with(mocker.ANY, "3 otps extracted", mocker.ANY, FONT, FONT_SCALE, FONT_COLOR, FONT_THICKNESS, FONT_LINE_STYLE)
|
||||
elif success is None:
|
||||
assert captured.out == ''
|
||||
assert captured.err == ''
|
||||
mocked_polylines.assert_not_called()
|
||||
mocked_putText.assert_called_with(mocker.ANY, "0 otps extracted", mocker.ANY, FONT, FONT_SCALE, FONT_COLOR, FONT_THICKNESS, FONT_LINE_STYLE)
|
||||
else:
|
||||
assert captured.out == ''
|
||||
assert captured.err != ''
|
||||
mocked_polylines.assert_called_with(mocker.ANY, mocker.ANY, True, FAILURE_COLOR, mocker.ANY)
|
||||
mocked_putText.assert_called_with(mocker.ANY, "0 otps extracted", mocker.ANY, FONT, FONT_SCALE, FONT_COLOR, FONT_THICKNESS, FONT_LINE_STYLE)
|
||||
else:
|
||||
# Act
|
||||
with pytest.raises(SystemExit) as e:
|
||||
extract_otp_secrets.main([])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
expected_err_msg = 'error: the following arguments are required: infile'
|
||||
|
||||
assert expected_err_msg in captured.err
|
||||
assert captured.out == ''
|
||||
assert e.value.code == 2
|
||||
assert e.type == SystemExit
|
||||
|
||||
|
||||
def test_verbose_and_quiet(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
with pytest.raises(SystemExit) as e:
|
||||
# Act
|
||||
|
||||