From f4934192aea7ee2cdc66a7a765bad7fab5b90871 Mon Sep 17 00:00:00 2001 From: scito Date: Sat, 24 Dec 2022 01:59:35 +0100 Subject: [PATCH] WIP --- Pipfile | 10 +- Pipfile.lock | 278 +---------------------- README.md | 24 +- extract_otp_secret_keys.py | 143 ++++++------ requirements-dev.txt | 2 - test_extract_otp_secret_keys_pytest.py | 163 ++++++++++++- test_extract_otp_secret_keys_unittest.py | 28 ++- test_extract_qrcode_unittest.py | 37 ++- utils.py | 7 +- 9 files changed, 303 insertions(+), 389 deletions(-) diff --git a/Pipfile b/Pipfile index 5bdd714..899e049 100644 --- a/Pipfile +++ b/Pipfile @@ -4,15 +4,11 @@ verify_ssl = true name = "pypi" [packages] -protobuf = "==4.21.12" +protobuf = "*" qrcode = "*" pillow = "*" -wheel = "==0.38.4" -pytest = "==7.2.0" -flake8 = "==6.0.0" -pylint = "==2.15.9" -qreader = "==1.3.1" -opencv-python = "==4.6.0.66" +qreader = "*" +opencv-python = "*" [dev-packages] pytest = "*" diff --git a/Pipfile.lock b/Pipfile.lock index cb5fc84..ff5c753 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0aea0eade4cf314b23a91c56582b66626a03b15f8e089b568ad55186cf8e6163" + "sha256": "2f4059c8dbac6be85b1e3b2c2032b884d48dc6a7fd520ffdebb951e23246a23e" }, "pipfile-spec": 6, "requires": { @@ -16,102 +16,6 @@ ] }, "default": { - "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" - }, - "colorama": { - "hashes": [ - "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", - "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" - ], - "markers": "sys_platform == 'win32'", - "version": "==0.4.6" - }, - "dill": { - "hashes": [ - "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0", - "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373" - ], - "markers": "python_version < '3.11'", - "version": "==0.3.6" - }, - "exceptiongroup": { - "hashes": [ - "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828", - "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec" - ], - "markers": "python_version < '3.11'", - "version": "==1.0.4" - }, - "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" - }, "numpy": { "hashes": [ "sha256:0104d8adaa3a4cc60c2777cab5196593bf8a7f416eda133be1f3803dd0838886", @@ -143,7 +47,7 @@ "sha256:f9168790149f917ad8e3cf5047b353fefef753bd50b07c547da0bdf30bc15d91", "sha256:fe44e925c68fb5e8db1334bf30ac1a1b6b963b932a19cf41d2e899cf02f36aab" ], - "markers": "python_version >= '3.9'", + "markers": "python_version >= '3.10'", "version": "==1.24.0" }, "opencv-python": { @@ -159,14 +63,6 @@ "index": "pypi", "version": "==4.6.0.66" }, - "packaging": { - "hashes": [ - "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3", - "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3" - ], - "markers": "python_version >= '3.7'", - "version": "==22.0" - }, "pillow": { "hashes": [ "sha256:03150abd92771742d4a8cd6f2fa6246d847dcd2e332a18d0c15cc75bf6703040", @@ -234,22 +130,6 @@ "index": "pypi", "version": "==9.3.0" }, - "platformdirs": { - "hashes": [ - "sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca", - "sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e" - ], - "markers": "python_version >= '3.7'", - "version": "==2.6.0" - }, - "pluggy": { - "hashes": [ - "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" - ], - "markers": "python_version >= '3.6'", - "version": "==1.0.0" - }, "protobuf": { "hashes": [ "sha256:1f22ac0ca65bb70a876060d96d914dae09ac98d114294f77584b0d2644fa9c30", @@ -270,38 +150,6 @@ "index": "pypi", "version": "==4.21.12" }, - "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" - }, "pyzbar": { "hashes": [ "sha256:13e3ee5a2f3a545204a285f41814d5c0db571967e8d4af8699a03afc55182a9c", @@ -323,100 +171,6 @@ ], "index": "pypi", "version": "==1.3.1" - }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version < '3.11'", - "version": "==2.0.1" - }, - "tomlkit": { - "hashes": [ - "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b", - "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73" - ], - "markers": "python_version >= '3.6'", - "version": "==0.11.6" - }, - "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" } }, "develop": { @@ -436,30 +190,14 @@ "markers": "python_version >= '3.6'", "version": "==22.2.0" }, - "colorama": { - "hashes": [ - "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", - "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" - ], - "markers": "sys_platform == 'win32'", - "version": "==0.4.6" - }, "dill": { "hashes": [ "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0", "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373" ], - "markers": "python_version < '3.11'", + "markers": "python_version >= '3.11'", "version": "==0.3.6" }, - "exceptiongroup": { - "hashes": [ - "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828", - "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec" - ], - "markers": "python_version < '3.11'", - "version": "==1.0.4" - }, "flake8": { "hashes": [ "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7", @@ -572,14 +310,6 @@ "index": "pypi", "version": "==7.2.0" }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version < '3.11'", - "version": "==2.0.1" - }, "tomlkit": { "hashes": [ "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b", @@ -663,7 +393,7 @@ "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015", "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af" ], - "markers": "python_version < '3.11'", + "markers": "python_version >= '3.11'", "version": "==1.14.1" } } diff --git a/README.md b/README.md index 2df19de..86d3670 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ cd extract_otp_secret_keys
usage: extract_otp_secret_keys.py [-h] [--json FILE] [--csv FILE] [--keepass FILE] [--printqr] [--saveqr DIR] [--verbose | --quiet] infile
 
 positional arguments:
-  infile                   file or - for stdin with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored
+  infile                   1) file or - for stdin with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored; 2) image file containing a QR code or = for stdin for an image containing a QR code
 
 options:
   -h, --help               show this help message and exit
@@ -57,11 +57,27 @@ Known to work with
 
 For protobuf versions 3.14.0 or similar or Python 3.6, use the extract_otp_secret_keys version 1.4.0.
 
-### Optional
+## Examples
 
-For printing QR codes, the qrcode module is required, otherwise it can be omitted.
+### Printing otp secrets form text file
 
-    pip install qrcode[pil]
+    python extract_otp_secret_keys.py example_export.txt
+
+### Printing otp secrets from stdin
+
+    python extract_otp_secret_keys.py - < example_export.txt
+
+### Printing otp secrets from image
+
+    python extract_otp_secret_keys.py test/test_googleauth_export.png
+
+### Printing otp secrets from stdin (image)
+
+    python extract_otp_secret_keys.py = < test/test_googleauth_export.png
+
+### Printing otp secrets csv to stdout
+
+    python extract_otp_secret_keys.py --csv - example_export.txt
 
 ## Features
 
diff --git a/extract_otp_secret_keys.py b/extract_otp_secret_keys.py
index 47d8012..311c38a 100644
--- a/extract_otp_secret_keys.py
+++ b/extract_otp_secret_keys.py
@@ -41,24 +41,22 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see .
 
+# TODO optimze imports
 import argparse
 import base64
 import fileinput
 import sys
 import csv
 import json
-import cv2
+from cv2 import imread, imdecode, IMREAD_UNCHANGED
 from qreader import QReader
 from urllib.parse import parse_qs, urlencode, urlparse, quote
 from os import path, makedirs
 from re import compile as rcompile
+from numpy import frombuffer
 import protobuf_generated_python.google_auth_pb2
 
 
-verbose = False
-quiet = True
-
-
 def sys_main():
     main(sys.argv[1:])
 
@@ -82,19 +80,12 @@ def main(sys_args):
 def parse_args(sys_args):
     formatter = lambda prog: argparse.HelpFormatter(prog, max_help_position=52)
     arg_parser = argparse.ArgumentParser(formatter_class=formatter)
-    arg_parser.add_argument('infile',
-                            help="image file containing a QR code from a Google Authenticator export or a text file "
-                                 "or - for stdin with \"otpauth-migration://...\" URLs separated by newlines. Lines "
-                                 "starting with # are ignored.")
+    arg_parser.add_argument('infile', help='1) file or - for stdin with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored; 2) image file containing a QR code or = for stdin for an image containing a QR code')
     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('--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'))
     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')
@@ -110,23 +101,18 @@ def extract_otps(args):
 
     otps = []
 
-    lines = get_lines_from_file(args.infile)
-
     i = j = 0
 
-    for line in lines:
-        if verbose:
-            print(line)
-        if line.startswith('#') or line == '':
-            continue
+    for line in get_lines_from_file(args.infile):
+        if verbose: print(line)
+        if line.startswith('#') or line == '': continue
         i += 1
         payload = get_payload_from_line(line, i, args)
 
         # pylint: disable=no-member
         for raw_otp in payload.otp_parameters:
             j += 1
-            if verbose:
-                print('\n{}. Secret Key'.format(j))
+            if verbose: print('\n{}. Secret Key'.format(j))
             secret = convert_secret_from_bytes_to_base32_str(raw_otp.secret)
             otp_type_enum = get_enum_name_by_number(raw_otp, 'type')
             otp_type = get_otp_type_str_from_code(raw_otp.type)
@@ -153,57 +139,61 @@ def extract_otps(args):
     return otps
 
 
-def get_lines_from_file(filepath):
+def get_lines_from_file(filename):
     global verbose
 
-    # Check if this is an image file
-    if(path.splitext(filepath)[1][1:].lower() in ('bmp', 'jpg', 'jpeg', 'png', 'tif', 'tiff')):
-        # It's an image file, so try to read it as a QR Code
+    if filename != '=':
+        check_file_exists(filename)
+        finput = fileinput.input(filename)
         try:
-            decoder = QReader()
+            lines = []
+            for line in (line.strip() for line in finput):
+                # TODO improve
+                # if verbose: print(line)
+                # if line.startswith('#') or line == '':
+                #     continue
+                # unfortunately yield line leads to random test fails
+                lines.append(line)
+            return lines
+        except UnicodeDecodeError:
+            if filename == '-':
+                abort('\nERROR: 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
+        finally:
+            finput.close()
 
-            if not path.isfile(filepath):
-                eprint('\nERROR: Input file provided is non-existent or not a file.'
-                       '\ninput file: {}'.format(filepath))
-                return []
+    # could not process text file, try reading as image
+    if filename != '-':
+        try:
+            if filename != '=':
+                image = 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()
+                array = frombuffer(stdin, dtype='uint8')
+                image = imdecode(array, IMREAD_UNCHANGED)
 
-            image = cv2.imread(filepath)
             if image is None:
-                eprint('\nERROR: Unable to open file for reading. Please ensure that you have read access to the '
-                       'file and that the file is a valid image file.\ninput file: {}'.format(filepath))
-                return []
+                abort('\nERROR: Unable to open file for reading.\ninput file: {}'.format(filename))
 
+            decoder = QReader()
             decoded_text = decoder.detect_and_decode(image=image)
             if decoded_text is None:
-                eprint('\nERROR: Unable to read QR Code from file.\ninput file: {}'.format(filepath))
-                return []
+                abort('\nERROR: Unable to read QR Code from file.\ninput file: {}'.format(filename))
 
             return [decoded_text]
         except Exception as e:
-            eprint('\nERROR: Encountered exception "{}".\ninput file: {}'.format(str(e), filepath))
-            return []
-    else:
-        # Not an image file, so assume it's a text file and proceed as usual
-        lines = []
-        finput = fileinput.input(filepath)
-        try:
-            for line in (line.strip() for line in finput):
-                if verbose:
-                    print(line)
-                if line.startswith('#') or line == '':
-                    continue
-                lines.append(line)
-        finally:
-            finput.close()
-        return lines
+            abort('\nERROR: Encountered exception "{}".\ninput file: {}'.format(str(e), filename))
 
 
 def get_payload_from_line(line, i, args):
     global verbose
     if not line.startswith('otpauth-migration://'):
-        eprint(
-            '\nWARN: line is not a otpauth-migration:// URL\ninput file: {}\nline "{}"\nProbably a wrong file was given'.format(
-                args.infile, line))
+        eprint( '\nWARN: line is not a otpauth-migration:// URL\ninput file: {}\nline "{}"\nProbably a wrong file was given'.format( args.infile, line))
     parsed_url = urlparse(line)
     if verbose > 1: print('\nDEBUG: parsed_url={}'.format(parsed_url))
     try:
@@ -212,10 +202,7 @@ def get_payload_from_line(line, i, args):
         params = []
     if verbose > 1: print('\nDEBUG: querystring params={}'.format(params))
     if 'data' not in params:
-        eprint(
-            '\nERROR: no data query parameter in input URL\ninput file: {}\nline "{}"\nProbably a wrong file was given'.format(
-                args.infile, line))
-        sys.exit(1)
+        abort('\nERROR: no data query parameter in input URL\ninput file: {}\nline "{}"\nProbably a wrong file was given'.format( args.infile, line))
     data_base64 = params['data'][0]
     if verbose > 1: print('\nDEBUG: data_base64={}'.format(data_base64))
     data_base64_fixed = data_base64.replace(' ', '+')
@@ -225,9 +212,8 @@ def get_payload_from_line(line, i, args):
     try:
         payload.ParseFromString(data)
     except:
-        eprint('\nERROR: Cannot decode otpauth-migration migration payload.')
-        eprint('data={}'.format(data_base64))
-        exit(1)
+        abort('\nERROR: Cannot decode otpauth-migration migration payload.\n'
+        'data={}'.format(data_base64))
     if verbose:
         print('\n{}. Payload Line'.format(i), payload, sep='\n')
 
@@ -252,8 +238,7 @@ def build_otp_url(secret, raw_otp):
     url_params = {'secret': secret}
     if raw_otp.type == 1: url_params['counter'] = raw_otp.counter
     if raw_otp.issuer: url_params['issuer'] = raw_otp.issuer
-    otp_url = 'otpauth://{}/{}?'.format(get_otp_type_str_from_code(raw_otp.type), quote(raw_otp.name)) + urlencode(
-        url_params)
+    otp_url = 'otpauth://{}/{}?'.format(get_otp_type_str_from_code(raw_otp.type), quote(raw_otp.name)) + urlencode( url_params)
     return otp_url
 
 
@@ -274,8 +259,7 @@ def save_qr(otp, args, j):
     pattern = rcompile(r'[\W_]+')
     file_otp_name = pattern.sub('', otp['name'])
     file_otp_issuer = pattern.sub('', otp['issuer'])
-    save_qr_file(args, otp['url'],
-                 '{}/{}-{}{}.png'.format(dir, j, file_otp_name, '-' + file_otp_issuer if file_otp_issuer else ''))
+    save_qr_file(args, otp['url'], '{}/{}-{}{}.png'.format(dir, j, file_otp_name, '-' + file_otp_issuer if file_otp_issuer else ''))
     return file_otp_issuer
 
 
@@ -330,8 +314,7 @@ def write_keepass_csv(args, otps):
                         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 = csv.DictWriter(outfile, ["Title", "User Name", "HmacOtp-Secret-Base32", "HmacOtp-Counter", "Group"])
                 writer.writeheader()
                 for otp in otps:
                     if otp['type'] == 'hotp':
@@ -344,10 +327,8 @@ def write_keepass_csv(args, otps):
                         })
                         count_hotp_entries += 1
         if not quiet:
-            if count_totp_entries > 0: print(
-                "Exported {} totp entries to keepass csv file {}".format(count_totp_entries, otp_filename_totp))
-            if count_hotp_entries > 0: print(
-                "Exported {} hotp entries to keepass csv file {}".format(count_hotp_entries, otp_filename_hotp))
+            if count_totp_entries > 0: print( "Exported {} totp entries to keepass csv file {}".format(count_totp_entries, otp_filename_totp))
+            if count_hotp_entries > 0: print( "Exported {} hotp entries to keepass csv file {}".format(count_hotp_entries, otp_filename_hotp))
 
 
 def write_json(args, otps):
@@ -386,10 +367,20 @@ def open_file_or_stdout_for_csv(filename):
     return open(filename, "w", encoding='utf-8', newline='') if filename != '-' else sys.stdout
 
 
+def check_file_exists(filename):
+    if filename != '-' and not path.isfile(filename):
+        abort('\nERROR: Input file provided is non-existent or not a file.'
+        '\ninput file: {}'.format(filename))
+
 def eprint(*args, **kwargs):
     '''Print to stderr.'''
     print(*args, file=sys.stderr, **kwargs)
 
 
+def abort(*args, **kwargs):
+    eprint(*args, **kwargs)
+    sys.exit(1)
+
+
 if __name__ == '__main__':
     sys_main()
diff --git a/requirements-dev.txt b/requirements-dev.txt
index cd0cf17..8202127 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -2,5 +2,3 @@ wheel
 pytest
 flake8
 pylint
-qreader
-opencv-python
diff --git a/test_extract_otp_secret_keys_pytest.py b/test_extract_otp_secret_keys_pytest.py
index 110d239..99d8bf6 100644
--- a/test_extract_otp_secret_keys_pytest.py
+++ b/test_extract_otp_secret_keys_pytest.py
@@ -18,11 +18,11 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see .
 
-from utils import read_csv, read_csv_str, read_json, read_json_str, remove_files, remove_dir_with_files, read_file_to_str, file_exits
+from utils import read_csv, read_csv_str, read_json, read_json_str, remove_files, remove_dir_with_files, read_file_to_str, read_binary_file_as_stream, file_exits
 from os import path
 from pytest import raises, mark
-from io import StringIO
-from sys import implementation
+from io import StringIO, BytesIO
+from sys import implementation, stdin
 
 import extract_otp_secret_keys
 
@@ -38,6 +38,22 @@ def test_extract_stdout(capsys):
     assert captured.err == ''
 
 
+def test_extract_non_existent_file(capsys):
+    # Act
+    with raises(SystemExit) as e:
+        extract_otp_secret_keys.main(['test/non_existent_file.txt'])
+
+    # Assert
+    captured = capsys.readouterr()
+
+    expected_stderr = '\nERROR: Input file provided is non-existent or not a file.\ninput file: test/non_existent_file.txt\n'
+
+    assert captured.err == expected_stderr
+    assert captured.out == ''
+    assert e.value.code == 1
+    assert e.type == SystemExit
+
+
 def test_extract_stdin_stdout(capsys, monkeypatch):
     # Arrange
     monkeypatch.setattr('sys.stdin', StringIO(read_file_to_str('example_export.txt')))
@@ -335,7 +351,7 @@ def test_extract_debug(capsys):
 
 
 def test_extract_help(capsys):
-    with raises(SystemExit) as pytest_wrapped_e:
+    with raises(SystemExit) as e:
         # Act
         extract_otp_secret_keys.main(['-h'])
 
@@ -345,13 +361,13 @@ def test_extract_help(capsys):
     assert len(captured.out) > 0
     assert "-h, --help" in captured.out and "--verbose, -v" in captured.out
     assert captured.err == ''
-    assert pytest_wrapped_e.type == SystemExit
-    assert pytest_wrapped_e.value.code == 0
+    assert e.type == SystemExit
+    assert e.value.code == 0
 
 
 def test_extract_no_arguments(capsys):
     # Act
-    with raises(SystemExit) as pytest_wrapped_e:
+    with raises(SystemExit) as e:
         extract_otp_secret_keys.main([])
 
     # Assert
@@ -361,10 +377,12 @@ def test_extract_no_arguments(capsys):
 
     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):
-    with raises(SystemExit) as pytest_wrapped_e:
+    with raises(SystemExit) as e:
         # Act
         extract_otp_secret_keys.main(['-v', '-q', 'example_export.txt'])
 
@@ -374,10 +392,12 @@ def test_verbose_and_quiet(capsys):
     assert len(captured.err) > 0
     assert 'error: argument --quiet/-q: not allowed with argument --verbose/-v' in captured.err
     assert captured.out == ''
+    assert e.value.code == 2
+    assert e.type == SystemExit
 
 
 def test_wrong_data(capsys):
-    with raises(SystemExit) as pytest_wrapped_e:
+    with raises(SystemExit) as e:
         # Act
         extract_otp_secret_keys.main(['test/test_export_wrong_data.txt'])
 
@@ -391,10 +411,12 @@ data=XXXX
 
     assert captured.err == expected_stderr
     assert captured.out == ''
+    assert e.value.code == 1
+    assert e.type == SystemExit
 
 
 def test_wrong_content(capsys):
-    with raises(SystemExit) as pytest_wrapped_e:
+    with raises(SystemExit) as e:
         # Act
         extract_otp_secret_keys.main(['test/test_export_wrong_content.txt'])
 
@@ -415,6 +437,8 @@ Probably a wrong file was given
 
     assert captured.out == ''
     assert captured.err == expected_stderr
+    assert e.value.code == 1
+    assert e.type == SystemExit
 
 
 def test_wrong_prefix(capsys):
@@ -448,6 +472,125 @@ def test_add_pre_suffix(capsys):
     assert extract_otp_secret_keys.add_pre_suffix("name", "totp") == "name.totp"
 
 
+def test_img_qr_reader_from_file_happy_path(capsys):
+    # Act
+    extract_otp_secret_keys.main(['test/test_googleauth_export.png'])
+
+    # Assert
+    captured = capsys.readouterr()
+
+    expected_stdout =\
+'''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
+
+'''
+
+    assert captured.out == expected_stdout
+    assert captured.err == ''
+
+
+def test_img_qr_reader_from_stdin(capsys, monkeypatch):
+    # Arrange
+    # sys.stdin.buffer should be monkey patched, but it does not work
+    monkeypatch.setattr('sys.stdin', read_binary_file_as_stream('test/test_googleauth_export.png'))
+
+    # Act
+    extract_otp_secret_keys.main(['='])
+
+    # Assert
+    captured = capsys.readouterr()
+
+    expected_stdout =\
+'''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
+
+'''
+
+    assert captured.out == expected_stdout
+    assert captured.err == ''
+
+
+def test_img_qr_reader_no_qr_code_in_image(capsys):
+    # Act
+    with raises(SystemExit) as e:
+        extract_otp_secret_keys.main(['test/lena_std.tif'])
+
+    # Assert
+    captured = capsys.readouterr()
+
+    expected_stderr = '\nERROR: Unable to read QR Code from file.\ninput file: test/lena_std.tif\n'
+
+    assert captured.err == expected_stderr
+    assert captured.out == ''
+    assert e.value.code == 1
+    assert e.type == SystemExit
+
+
+def test_img_qr_reader_nonexistent_file(capsys):
+    # Act
+    with raises(SystemExit) as e:
+        extract_otp_secret_keys.main(['test/nonexistent.bmp'])
+
+    # Assert
+    captured = capsys.readouterr()
+
+    expected_stderr = '\nERROR: Input file provided is non-existent or not a file.\ninput file: test/nonexistent.bmp\n'
+
+    assert captured.err == expected_stderr
+    assert captured.out == ''
+    assert e.value.code == 1
+    assert e.type == SystemExit
+
+
+def test_non_image_file(capsys):
+    # Act
+    with raises(SystemExit) as e:
+        extract_otp_secret_keys.main(['test/text_masquerading_as_image.jpeg'])
+
+    # Assert
+    captured = capsys.readouterr()
+    expected_stderr = '''
+WARN: line is not a otpauth-migration:// URL
+input file: test/text_masquerading_as_image.jpeg
+line "This is just a text file masquerading as an image file."
+Probably a wrong file was given
+
+ERROR: no data query parameter in input URL
+input file: test/text_masquerading_as_image.jpeg
+line "This is just a text file masquerading as an image file."
+Probably a wrong file was given
+'''
+
+    assert captured.err == expected_stderr
+    assert captured.out == ''
+    assert e.value.code == 1
+    assert e.type == SystemExit
+
+
 def cleanup():
     remove_files('test_example_*.csv')
     remove_files('test_example_*.json')
diff --git a/test_extract_otp_secret_keys_unittest.py b/test_extract_otp_secret_keys_unittest.py
index 641c1df..76cc9c9 100644
--- a/test_extract_otp_secret_keys_unittest.py
+++ b/test_extract_otp_secret_keys_unittest.py
@@ -187,19 +187,41 @@ Type:    totp
         self.assertGreater(len(actual_output), len(expected_stdout))
         self.assertTrue("DEBUG: " in actual_output)
 
-    def test_extract_help(self):
+    def test_extract_help_1(self):
         out = io.StringIO()
         with redirect_stdout(out):
             try:
                 extract_otp_secret_keys.main(['-h'])
-            except SystemExit:
-                pass
+                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):
+        out = io.StringIO()
+        with redirect_stdout(out):
+            with self.assertRaises(SystemExit) as context:
+                extract_otp_secret_keys.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):
+        with Capturing() as actual_output:
+            with self.assertRaises(SystemExit) as context:
+                extract_otp_secret_keys.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):
         self.cleanup()
 
diff --git a/test_extract_qrcode_unittest.py b/test_extract_qrcode_unittest.py
index f753ae3..2b62137 100644
--- a/test_extract_qrcode_unittest.py
+++ b/test_extract_qrcode_unittest.py
@@ -23,9 +23,8 @@ from utils import Capturing
 
 import extract_otp_secret_keys
 
-
 class TestExtract(unittest.TestCase):
-    def test_happy_path(self):
+    def test_img_qr_reader_happy_path(self):
         with Capturing() as actual_output:
             extract_otp_secret_keys.main(['test/test_googleauth_export.png'])
 
@@ -36,34 +35,48 @@ class TestExtract(unittest.TestCase):
 
         self.assertEqual(actual_output, expected_output)
 
-    def test_no_qr_code_in_image(self):
+    def test_img_qr_reader_no_qr_code_in_image(self):
         with Capturing() as actual_output:
-            extract_otp_secret_keys.main(['test/lena_std.tif'])
+            with self.assertRaises(SystemExit) as context:
+                extract_otp_secret_keys.main(['test/lena_std.tif'])
 
         expected_output =\
         ['', 'ERROR: Unable to read QR Code from file.', 'input file: test/lena_std.tif']
 
         self.assertEqual(actual_output, expected_output)
+        self.assertEqual(context.exception.code, 1)
 
-    def test_nonexistent_file(self):
+    def test_img_qr_reader_nonexistent_file(self):
         with Capturing() as actual_output:
-            extract_otp_secret_keys.main(['test/nonexistent.bmp'])
+            with self.assertRaises(SystemExit) as context:
+                extract_otp_secret_keys.main(['test/nonexistent.bmp'])
 
         expected_output =\
         ['', 'ERROR: Input file provided is non-existent or not a file.', 'input file: test/nonexistent.bmp']
 
         self.assertEqual(actual_output, expected_output)
+        self.assertEqual(context.exception.code, 1)
 
-
-    def test_non_image_file(self):
+    def test_img_qr_reader_non_image_file(self):
         with Capturing() as actual_output:
-            extract_otp_secret_keys.main(['test/text_masquerading_as_image.jpeg'])
+            with self.assertRaises(SystemExit) as context:
+                extract_otp_secret_keys.main(['test/text_masquerading_as_image.jpeg'])
 
-        expected_output =\
-        ['', 'ERROR: Unable to open file for reading. Please ensure that you have read access to the file and that '
-             'the file is a valid image file.', 'input file: test/text_masquerading_as_image.jpeg']
+        expected_output = [
+            '',
+            'WARN: line is not a otpauth-migration:// URL',
+            'input file: test/text_masquerading_as_image.jpeg',
+            'line "This is just a text file masquerading as an image file."',
+            'Probably a wrong file was given',
+            '',
+            'ERROR: no data query parameter in input URL',
+            'input file: test/text_masquerading_as_image.jpeg',
+            'line "This is just a text file masquerading as an image file."',
+            'Probably a wrong file was given'
+        ]
 
         self.assertEqual(actual_output, expected_output)
+        self.assertEqual(context.exception.code, 1)
 
     def setUp(self):
         self.cleanup()
diff --git a/utils.py b/utils.py
index 839864d..227d746 100644
--- a/utils.py
+++ b/utils.py
@@ -17,7 +17,7 @@ import csv
 import json
 import os
 import shutil
-from io import StringIO
+from io import StringIO, BytesIO
 import sys
 import glob
 
@@ -102,3 +102,8 @@ def read_file_to_list(filename):
 def read_file_to_str(filename):
     """Returns a str."""
     return "".join(read_file_to_list(filename))
+
+def read_binary_file_as_stream(filename):
+    """Returns binary file content."""
+    with open(filename, "rb",) as infile:
+        return BytesIO(infile.read())