-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 5b11e8b
Showing
9 changed files
with
938 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
venv/ | ||
dist/ | ||
demo/out | ||
*.pyc |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# totp-qr-backup | ||
|
||
Python CLI script for making printer-friendly backups of your TOTP QR codes to print out and store somewhere safe. | ||
|
||
## Quick start | ||
|
||
```shell | ||
# Install | ||
pip install git+https://github.com/jakekara/totp-qr-backup | ||
|
||
# Start the webcam QR code scanner (press Q to quit when you're done) | ||
# This examples tores in a directory called `out` | ||
qr-backup scan out | ||
|
||
# Create an HTML digest and store it in digest.html | ||
qr-backup digest out > digest.html | ||
|
||
# Now you can open digest.html in a browser and print it out. | ||
``` | ||
|
||
## Sample output | ||
|
||
[Here](demo/digest.html) is the digest.html file created in the above quickstart example. | ||
|
||
The input QR codes came from [this gist](https://gist.github.com/kcramer/c6148fb906e116d84e4bde7b2ab56992) | ||
|
||
## Project status | ||
|
||
This is pretty much good enough for my own usage, but I don't think I'll do much in the way of supporting this unless people end up using it, in which case I'll be glad to make some improvements. | ||
|
||
The code could be a lot better organized, but meh. I was learning all of this as I went: How to scan QR codes, how to use the web cam from Python, how to generate QR codes, how to parse TOTP URIs. | ||
|
||
## Why? | ||
|
||
Losing all my two-factor codes, like, by losing my phone, would suck. Paper is a good way to keep backups of important information. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
|
||
|
||
<style> | ||
@media print { | ||
div.qr-section { | ||
break-inside: avoid; | ||
} | ||
} | ||
</style> | ||
|
||
|
||
<div class='qr-section'><h2>ACME Co - jdoe@example.com</h2><div style='display:flex'> <div><img width='200px' src='data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAjoAAAI6AQAAAAAGM99tAAAFR0lEQVR4nO2dTY6kSAyFnwekWoI0B6ijwM1afaS5ARylDlBSsGwJ5FmEI8JB9ao7c8Yz+VjQSQGfQHJbL/yHKB6y7X88hgMQRBBBBBFEEEEEEUTQ/wUkto0ADvsl6yEiKwBZgXwC+3yJyAw7K2KXxH01gl4UBFVVxaKqqmlQ1QToNp1lh0HdL1W1JeeSyuGiqrrFezWCXho02r/HDCx/AdJOLR8jBNOnABghS5oFmD5HLNuVbxNgOB/9RAQR9Iht/Olfl483xb4CCkCLtWcrVuzzJxTHk56IIIKeAlrSoCLvJ2TFoNhFBMCg+n2+BMCVD//BJyKIoF/ZbjobyMJaFUsCmvbGkgYFphP5EqXOJug/ANpFcswDSwLk28cIAIPKCkDW6qSXj7ds8rLiyqGRJz0RQQT91pZ19s9S7LrPg+2ypp7O8rf3E8DU3xTv1Qh6aVBRIwlAVhlqAT8seiILEQAwcaJnvqOcMElCNUJQMBBKiLqaaDNq1SKsp9uJE6auadkEBQU5D1wWirjnZ2zx2Odstqm38XivRtBLg6rPRjZR888wU87qeptKPgaTKha1Q90KgJZNUDBQ8dl91rypDE3DPfeeRXnOrteztGyCYoHqCrJKjSKss2s2cQInu9uqskgSWjZB4UB+BbkByEkawHyx/UqWvWn1Uvls9t60bILigYrLrcaKasB5eZia2K7ipAgWc+G0bILigYphthBIsWI7P1n+3K0bqygv/wNo2QSFAzmdXYT1UFx46oJ72cZTDWADsDto2QTFA1U1Us07WR0UqtguGgRw8sPdRssmKB6oxjyaudaISAmQmH9OgFPcfa6Slk1QMNCXupENqJZ9M+qmRlpVCbPrBMUE+fqmVhSClljX01+91LNa6qVo2QRFBLl4dj52cjoBraTPdEnV43YxfTZBkUGq6ZISz7auGVknVVlz/8wluh1vCuAS7DPQLpb1GU9EEEGPAEnumjlGZLPN7Y6Htf/KCkBkvkS+1Zhgvpg9NQQFBZWemtI1A2BQ7OsIBX6I4gAUx3ACx595J1WwgFMZCIoK+lLr5yLWtbgPqPXZrlwbzNQQFBfUVURN1Z5LXC+75sUdWsg779hTQ1BUkK8bqQGSllMvhU990jF1cW9aNkEBQV12vbb01rYCdQ2/CV3z2KTsgyQoLsjlILMpu84wdDWuFuNuLvx0ldq0bIKCgZwaMZ3dCkXyia4J2JnyVILftGyCAoK6iqjUhqG1lt7WzevHnbWSV1o2QRFBvd6oNam3WQzwg7VbMRR9NkFhQZ3UAFCKodrQkaLCvc++KW5aNkHhQE6N3IaJdLNY+z6bPhJIyyYoIMipka53HTWeXXp9W0dkPpt6QLxXI4ggAMdbdsOyHiK6TVbm565cPmr106SK/b2bZxn41Qh6SZDL1LRBC/A1Iqi9vu06uMAgV5AERQTVeHbXg9DCItkru0kN3cxWzhshKCqomzLctajbqrJdV6pFXJqS8WyCooL8jAWty8Ot7mpsxJl8rV0FJ8MTFBvUvuHbxqtmh7zP9k0a15eA2oAD1C/8Rn01gl4U9OXbYjWud58t3Np8rdG9u44+m6CYoKN+QP0Yi6bObZGXiMwlu/5dRoi8q30o0saUPOWJCCLotzbz2Ql+7qr58ZJ0/DI3Sn3QhDqboICgL5btYtctvO3qs+F+lWE7tGyCwoJuGZh0CTD9kPZpU99QdoyQtetQiPxqBL0kyH+ByQU+JjUNUg/9/Ox0G5pGn01QOND9G77qD0vDr+1qGUn7fG/7Fe/VCCKIIIIIIogggggi6F8D/Q33anDSb+wDdwAAAABJRU5ErkJggg==' /><br/> </div> <div><code>issuer=ACME Co</code><br/><code>name=jdoe@example.com</code><br/><code>secret=AUSJD7LZ5H27TAC7NW2IJMATDMVDUPUG</code><br/><code>digest=<built-in function openssl_sha1></code><br/><code>digits=6</code><br/><br><code>uri=otpauth://totp/ACME%20Co:jdoe@example.com?secret=AUSJD7LZ5H27TAC7NW2IJMATDMVDUPUG&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30</code> </div></div></div><div class='qr-section'><h2>DropBox - jdoe@gmail.com</h2><div style='display:flex'> <div><img width='200px' src='data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAhIAAAISAQAAAACxRhsSAAAE5UlEQVR4nO2dUYrjOBCG/1ob8qjAHCBHcW4wR2r2SHMD6yg5wIDzGLCpfVBVSe6dzTRMIrLh10PTjuUPGYqSqvSrLIo/bfmvP0YAZJBBBhlkkEEGGWSQURlibQRwFRE5AuUyywgAmwDXEeXG+er9zw8eBxlk/EdTVVVMqqq6DBZVTcugqrqi3tC5dF5RLutj86u8Cxnvz7iad9Q5rdAZgMhxUOAa/jStzXPmfB8+DjLI+BJjuoxAPt1E56sIkG4ichxUPhYA+QjIucc4yCCjaeOna5lmhSABgvRzRBYA07yJ4vot3Ovjx0EGGfeam11SAFdAi2H+OKhMP0ZgmgHB9ZsKAMGksH4PHgcZZHyBkcWj/I8IpoChdaDTYpdyxlbC/cePgwwyftmKFVbvqPkIUeAmmo+w6CmfbqL5tALAJrr3pq/zLmS8L8PzUsugno3yNif1NFVkqOZk/2HSFdbvVd6FjPdlmD8N3ylIC5DPgALbqPn7AusiAJAQl4PKND9qHGSQcbe1CXsAqgvgudJBgWR3gWR+tsmkMs9PRh+Gz90Y1Kb3ZVDVxSy2TPQoK4DaBTH5007J6MGI9SnKLlRzYw5/Wjyr/9G5eFauT8noxjBb02WIyd+29n0Z4O51rp0bD0w7JaMHw31i2dUfimcFEJO/ZwOKt61RPnUoZHRk/GJ9CktE1S5mwEMJodz5royjyOjFaKdyC5zcve4nf4vyfWnaJFtpp2Q8nWEze5noI5NvMZMJTqt1mgEvfoP+lIw+jFifurHGIvVzgGWJ1Qi1ogvtlIynM8p+lEwXAZB+ihZNNIbVbsw30SyA5u8KAbYRk25iCquHjYMMMu62Jo4C0ET0FjiVNsTSIAKsxHmfjP6MTWx7FADycRNPRMVGaboJ8tF/s/zVw8dBBhl3GGlF0ZpmOah8LIPKGUBzPmq6jJBz1aReeT6KjG6MJt7f+cm4bFYAnkSNG4yjyOjEiLA+fkieSa3HpEM+1WhSQ0hFOyXj+Yw2LzV5Yr+0uilli9SaRHXLpp2S0YfR+FMrJ1Hi+Cgn4b+VrGmT519AOyWjF6Pdj5o9id/WQ6m1T2J/f4bvr3J9SkYfRuj523o81URtA8pXBS5HjQo+tFMy+jJcaZJljKI8WymKYqVQ5KBVqgIgUlcv9y5kvB2j2Y9qBPwmOB1C6D80Ub5tAFAvRUY/Rtip+8mSP63hf5yPqunU8thCHQoZ3RihPzWJdJXtmyZad0L/4mhTPEE7JaMLY6/nb/3pp7yUu9JWyk87JaMTY3faaQHaIrx+vMQCp1R10vu6Ka/yLmS8L6NZn9bzUa3t7nznp7rTtFMyOjHQmuMCIET9bX2puNHs+YN2SkY3xu4cX2OxbpMR4CPOTDWaVK5PyejJqN870b+PgyKfVsgZgMlMa2n+pKY/nS7Un5LRm2HrTjTJKVNOhYDa8lLxRYnky9pXexcy3pjh3zvxOtGbiJxugiwinqtaoXOR/FuXJ4yDDDK+wtA5qcrH5aCql4MlVvNp9S+eJfUj/qUC+rPGQQYZbfvX906QthH5dBNM8wrxY9JekxcApstBBUkhjxsHGWTcbR7l1w2oqIIC7MR9AGqyn3XPyejJCJ00gLopaqIpoBZAqYkoz/0zL0VGN4bo7/v8puVXeRcyyCCDDDLIIIMMMv7/jH8ATwoXeUtqXAkAAAAASUVORK5CYII=' /><br/> </div> <div><code>issuer=DropBox</code><br/><code>name=jdoe@gmail.com</code><br/><code>secret=VZL7MUKAT2N7ONO3GIQ3I3LZOMHNBCBL</code><br/><code>digest=<built-in function openssl_sha1></code><br/><code>digits=6</code><br/><br><code>uri=otpauth://totp/DropBox:jdoe@gmail.com?secret=VZL7MUKAT2N7ONO3GIQ3I3LZOMHNBCBL&issuer=DropBox&algorithm=SHA1&digits=6&period=30</code> </div></div></div><div class='qr-section'><h2>Amazon - jdoe@gmail.com</h2><div style='display:flex'> <div><img width='200px' src='data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAhIAAAISAQAAAACxRhsSAAAE8klEQVR4nO2dTW7jOBCFXw0FeCkBOYCPQt2skSPlBtJRfIABpGUACjULVpF04k4P0DbHk35cBLEkf6CAh2L9kRbF7471r99GAGSQQQYZZJBBBhlkkFEZYmMA1umQfGsVEZn3wR/dB2CdAJl3f36+8zzIIONLRlRV1Q1AvJzU1RkUGN+zgGUeEwAcoguCqqrqNeMe8yCDjNsjCy5uAOIW7D/AP5pYTcWqmqBabkRV1eVZ3oWMP4kxpvZP1HdB3ACRKajIBMjcZR5kkPEFY5WTGct1OgSrDAD2Ae4VDB+/8MTvQsa3Y4yqugBmQLGLoKzsHlsdUp9T1fSIeZBBxheMVUREJgDYT2qe6sVMKeIWVGYEBfYBMuPI4f7950EGGTdHXsbb4umYgPWcAIzvYh8niH00o3r3eZBBxpejifeBYIv6gqC6jGVlzzcsG5A9VXhyivE+GY9nFJ26L2o5qNAkoizy3yxDZU5qzgZQp2T0YJhOqxKjJuSICmMyy1oN6AI012hPyejEaOxpDeFzOr8u/tfWFijapT0low+jsadNPdRKUci1J7OixSGwJZ/rPhndGNf+qS352bJmEfojjbvaPkedktGDUexpsaKj+hoPwMKqktMv1X9d/DnqlIyOjCNnSFUvIvoq1i+lC6y039b83St4xDzIIOPGsHJ9XKBYBZD4FpJk2eIQ0+NupSddp02BHRBAvDzwLO9Cxh/CyMX7Q2rMJD8uQ17tddlPVjeV83uu9Mv8mHmQQcbHYXFUdUiBJhHlqVOrQlkBQJPHVvRPyejCuIqjav50Q4737ZpXUN3Q1rvUKRk9GJ5bGlt7akmnrV5p6/t5UKdkdGR8ru+jLeObHK8LqvU56pSMPgy3p9lYFjl6HFU3STUfyz4q+qdkdGKUbSSH9Zpif1HES05ESXVNxfpPgdKTes95kEHGv2GMKTf1AzhE5Oz2dBUR7zr17H5+jnkpMnozZN5PbX1/wSG+K6qUogDkpv5S+L/7PMgg4yfD4/1UeqNaN7TplyrxlnmqJYlK/5SMhzM+9Z/WfJM2ZrOkpMbrJlTqlIwujFJYSkAJ63PtaRktsWp3R88L1KIA81Jk9GG0ef7oGarsBmRNetOUm9xqbalTMroxSn2/CHO07acmVu/dd+0C1bJy3SejE6Pdd2KHnKFsKrFeU7O2W/nGVjtSqFMyejCaepSF+sUNsGxU0JqDamIr1qPI6MhoGvT8gL52q2m7if/j1+ifktGN0erU9kf5kl82mjRBf9yuoizqlIw+jKulXK8z+RtQm6a0bDWFpU6Z5yejM0N+aIKfJglrMlnPqnZtF8E6BdXXKSiAQ2RGYH2fjF6Mcl5fUACa26I0KoD4JkBcAMGYBgGGJMCQ7Nr+4k7rs7wLGd+XkXXqDXshKfYJAoSk61lNjuv0t+0tjW8vgEm07EZ9lnch49szsge6ASLnlJNOMpee1HgZIPMu0mziZ/8pGR0ZN3/vpOk1BYAmtqodfrHkCRhHkfFfMILm34qqxdPXsx0v7aUowE9Af+A8yCCjjBs/X5KPQhkAhCQYg0q8DEkwbipAUIlvIWE9J56HQkYvhut0VAA7oEAadJ02FYyuxPWcgLgcA+JyiAIJwD4w3iejF8P7UPJwrzR67l+1niG5fTqUgv4pGZ0Yor9+5hdjfZZ3IYMMMsgggwwyyCDj/8/4B9zaySgcEFIDAAAAAElFTkSuQmCC' /><br/> </div> <div><code>issuer=Amazon</code><br/><code>name=jdoe@gmail.com</code><br/><code>secret=VOHJXBL54OM2TTLKANOUUCJTE7PXVOIV</code><br/><code>digest=<built-in function openssl_sha1></code><br/><code>digits=6</code><br/><br><code>uri=otpauth://totp/Amazon:jdoe@gmail.com?secret=VOHJXBL54OM2TTLKANOUUCJTE7PXVOIV&issuer=Amazon&algorithm=SHA1&digits=6&period=30</code> </div></div></div><div class='qr-section'><h2>GitHub - jdoe@gmail.com</h2><div style='display:flex'> <div><img width='200px' src='data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAhIAAAISAQAAAACxRhsSAAAE2UlEQVR4nO2dXYrrOBCFT40NeZQhC+ilKDuYJQ29pN6BvZRZQMB+DMjUPKhKUtJzb1+4aY0nHD0Y23E+LCiqVD8qi+J3x/LHbyMAMsgggwwyyCCDDDLIqAyxMUJkAtqzaRcsUsYEyGXzq8uT34MMMn4wVFUVUVVV10H9XoLOAICQgKgp37OzddDmb/NR5kLG6zM2145xHRTYRiCu5RKDymU7qckuTOV+w3uQQcavMPR9ApAFM6jqvIlksV0me0QuHd6DDDLaMT5cS1wn0eXPmwDY/ddwEzvbpu95DzLI+Ol4WJ9Wy15/AFB+CP4c16dkdGSYONoYNC9Ivzr485RTMvowsmWvyVNd3hJ0mSDZ1c8H7PnSf7hPth5lLmS8LqOY9+Q3QoLOWURd0UatZwk6BzWlSn1KRh9GllOdAZPOuA6qc16G1qfM5NcQa5ZnpZyS0ZNRoqZNhPQSTMeqqvn7IhOyPtV5GyGXZ78HGWT8+2jd+pBcCAFPO5UcVbX2ddDuk9GH0cppXFHSo55GtUQp8tK0XtrSgHJKRkeGpZ2CWso0j3BrUlE6byfFMgEik+lYuTz5Pcgg4wfjLs4PU5vINn7wEH9I0BmmXt3VT7T7ZPRieJw/1INFTU0ws/HXIqzAQ1KKckrG9zNcn7rPZKJXQ/z3dX01ukp9SkZHxl2lSUiQ+HFOggAgfpxUAECxnRX5gB2CMKg0jKPMhYzXZZT8/uD+vqfx79Rmuz51f59xfjK6MYrdB9p1Z7Mq9YCVyWm5x3opMvoxzO4v05AEm2WdbGm6nZMAZuElrlOpSRUA2Ed93nuQQcZPR+M4xVKRYuUm7b0H7wkA/X0yujGyPpW4QgXbOQEYFMt0hQAnu7dMVwDbSa2ePz+3C+L8rPcgg4xfYliFad0Ghbj6vbgOKn/pTUTeSqxqhYdTDzcXMl6OUer6hrbWNN7tQKkJAAui5ufo75PRjVHyUanZgWKRJ9R6qUFVyyIVYFyKjL6MT9JZ01NzKfMrlVMWiCouFOWUjD6M4u8XpVpl8qF2f/UsAHAfTj3KXMh4XUYT5/cF6eB10vqpJtX/NCj3nZDRk9H6UVEf3KWh0bHZ2s+AB1FXsA6FjF4Mi5/muNR2TogfIwThCsV2BrDlYhRBVEAQVgi2swpC8hqWo8yFjNdltHXSJeNkavNei2anf0WzhYr6lIxODLP7JfzU2vNgtdMekrLnuD+KjO6MIqclJOpnvgMl4aExqu1KYZyfjH4Ml1P37ZsOUqX3iRn/KsUou6Ypp2R0YTT9UNzfLwVSQOvWW3uUGlNl/SkZ3Rhtf6laz49q9wHUwpN2UE7J6Mdo7H6W2LLVtAYCUPP7pdDfEwCUUzL6Mer3TnyRukv+1Enu1K8JWGSE9ZzKrSjKCuBYcyHjhRnNCjS3PfEwVe1/GldA32WE16nu/C4PGb0Z5XsnAHyl+vcILCICk1jvQpHvBaU+JeM/YyxvNzP5y7RLTkq9T+0ywFalG7/LQ0Y3xuP3TnS5nFTiDAjCdfRG54ACO4BwFSCskKi795o4ylzIeF1G6YeiADYAwC4KALpcRhNWbCMkzp6UWi4CLLUjylHmQsbrMj597wTBK1Ks7cRdQ/SmJ2oZjEuR8e0M0a+f+WIsR5kLGWSQQQYZZJBBBhn/f8Y/b3va6aR09L8AAAAASUVORK5CYII=' /><br/> </div> <div><code>issuer=GitHub</code><br/><code>name=jdoe@gmail.com</code><br/><code>secret=R6RY4IO257E23W74HUZS2BUTM5LCW2XT</code><br/><code>digest=<built-in function openssl_sha1></code><br/><code>digits=6</code><br/><br><code>uri=otpauth://totp/GitHub:jdoe@gmail.com?secret=R6RY4IO257E23W74HUZS2BUTM5LCW2XT&issuer=GitHub&algorithm=SHA1&digits=6&period=30</code> </div></div></div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
[build-system] | ||
requires = ["hatchling"] | ||
build-backend = "hatchling.build" | ||
|
||
[project] | ||
name = "totp-qr-backup" | ||
version = "0.0.1" | ||
|
||
dependencies = [ | ||
"pyzbar==0.1.9", | ||
"pillow==10.3.0", | ||
"pyotp==2.9.0", | ||
"opencv-python==4.9.0.80", | ||
"qrcode==7.4.2" | ||
] | ||
|
||
[project.scripts] | ||
qr-backup = "totp_qr_backup.cli:main" | ||
|
||
[tool.hatch.build.targets.sdist] | ||
only-include = ["totp_qr_backup"] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import argparse | ||
from totp_qr_backup import qrscan | ||
from totp_qr_backup import qrdigest | ||
|
||
def main(): | ||
|
||
parser = argparse.ArgumentParser( | ||
prog='qr-backup', | ||
description='Backup TOTP QR codes') | ||
|
||
subparsers = parser.add_subparsers() | ||
|
||
scan_parser = subparsers.add_parser("scan") | ||
scan_parser.set_defaults(func=qrscan.main) | ||
qrscan.add_arguments(scan_parser) | ||
|
||
digest_parser = subparsers.add_parser("digest") | ||
digest_parser.set_defaults(func=qrdigest.main) | ||
qrdigest.add_arguments(digest_parser) | ||
|
||
args = parser.parse_args() | ||
args.func(args) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import argparse | ||
from base64 import b64encode | ||
import io | ||
import os | ||
from textwrap import dedent | ||
from urllib.parse import parse_qs, urlparse | ||
|
||
import pyotp | ||
import qrcode | ||
|
||
def add_arguments(parser: argparse.ArgumentParser): | ||
parser.add_argument("qr_dir") | ||
|
||
# def get_args() -> argparse.Namespace: | ||
|
||
# parser = argparse.ArgumentParser() | ||
# parser.add_argument("qr_dir", type=str, help="qr directory") | ||
# return parser.parse_args() | ||
|
||
def make_html(dir_path): | ||
|
||
ret = "" | ||
for text_file_name in os.listdir(dir_path): | ||
|
||
if not text_file_name.endswith(".txt") and text_file_name is not None: | ||
continue | ||
|
||
text_file_path = os.path.join(dir_path, text_file_name) | ||
|
||
qr_text = open(text_file_path, "r").read() | ||
otp = pyotp.parse_uri(qr_text) | ||
|
||
ret += "<div class='qr-section'>" | ||
|
||
ret += f"<h2>{otp.issuer} - {otp.name}</h2>" | ||
|
||
ret += "<div style='display:flex'>" | ||
|
||
ret += " <div>" | ||
|
||
img_pil = qrcode.make(qr_text) | ||
img_binary = io.BytesIO() | ||
img_pil.save(img_binary, format="PNG") | ||
img_binary = img_binary.getvalue() | ||
img_b64 = b64encode(img_binary).decode("utf-8") | ||
|
||
|
||
ret += f"<img width='200px' src='data:image/png;base64, {img_b64}' /><br/>" | ||
|
||
ret += " </div>" | ||
|
||
ret += " <div>" | ||
|
||
keys_to_serialize = ["issuer", "name", "secret", "digest", "digits"] | ||
|
||
for key in keys_to_serialize: | ||
ret += f"<code>{key}={getattr(otp, key)}</code><br/>" | ||
|
||
ret += "<br>" | ||
ret += f"<code>uri={qr_text}</code>" | ||
ret += " </div>" | ||
|
||
ret += "</div>" | ||
|
||
ret += "</div>" | ||
|
||
return ret | ||
|
||
def html_style(): | ||
|
||
# Avoid breaking a qr section when printing | ||
|
||
return dedent( | ||
""" | ||
<style> | ||
@media print { | ||
div.qr-section { | ||
break-inside: avoid; | ||
} | ||
} | ||
</style> | ||
""") | ||
|
||
def html_top_section(): | ||
|
||
ret = "" | ||
ret += "<h1>QR Code Digest</h1>" | ||
ret += "<p>Print this document and store it in a secure location.<p>" | ||
|
||
return ret | ||
|
||
def main(args): | ||
|
||
qr_directory = args.qr_dir | ||
|
||
# print(html_top_section()) | ||
print(html_style()) | ||
print(make_html(qr_directory)) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import argparse | ||
from hashlib import sha256 | ||
import os | ||
from urllib.parse import parse_qs, unquote, urlparse | ||
|
||
import cv2 | ||
from pyzbar.pyzbar import decode | ||
import qrcode | ||
|
||
def add_arguments(parser:argparse.ArgumentParser): | ||
parser.add_argument("out_dir") | ||
|
||
def watch_for_qr_codes(): | ||
|
||
vid = cv2.VideoCapture(0) | ||
|
||
last_qr = None | ||
while(True): | ||
ret, frame = vid.read() | ||
cv2.imshow('frame', frame) | ||
if cv2.waitKey(1) & 0xFF == ord('q'): | ||
break | ||
|
||
try: | ||
decoded_qr = decode(frame) | ||
qr_text = decoded_qr[0].data.decode('ascii') | ||
except: | ||
continue | ||
|
||
if qr_text == last_qr: | ||
continue | ||
|
||
last_qr = qr_text | ||
yield qr_text | ||
|
||
def get_filename_from_totp(totp_str: str): | ||
|
||
hash = sha256(totp_str.encode("utf-8")).hexdigest()[:8] | ||
|
||
parsed_url = urlparse(totp_str) | ||
data = parse_qs(parsed_url.query) | ||
issuer = data["issuer"][0] | ||
path = unquote(parsed_url.path).strip().strip("/").replace("/","_").replace(":","_") | ||
|
||
fname = f"{issuer} - {path} - {hash}" | ||
|
||
return fname | ||
|
||
def main(args): | ||
|
||
out_dir = args.out_dir | ||
|
||
# Create the output directory if it doesn't exist | ||
os.makedirs(out_dir, exist_ok=True) | ||
|
||
for qr_text in watch_for_qr_codes(): | ||
file_name = get_filename_from_totp(qr_text) | ||
|
||
# Write the text file | ||
full_file_path = os.path.join(out_dir, f"{file_name}.txt") | ||
with open(full_file_path, "w") as fh: | ||
fh.write(qr_text) | ||
|
||
print(f"* Wrote QR code to {full_file_path}") | ||
|
||
# Write the png | ||
img = qrcode.make(qr_text) | ||
full_img_file_path = os.path.join(out_dir, f"{file_name}.png") | ||
img.save(full_img_file_path) |