Skip to content

Commit

Permalink
Initial
Browse files Browse the repository at this point in the history
  • Loading branch information
jakekara committed May 12, 2024
0 parents commit 5b11e8b
Show file tree
Hide file tree
Showing 9 changed files with 938 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
venv/
dist/
demo/out
*.pyc
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions README.md
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.
12 changes: 12 additions & 0 deletions demo/digest.html
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>
21 changes: 21 additions & 0 deletions pyproject.toml
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 added totp_qr_backup/__init__.py
Empty file.
22 changes: 22 additions & 0 deletions totp_qr_backup/cli.py
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)
101 changes: 101 additions & 0 deletions totp_qr_backup/qrdigest.py
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))

69 changes: 69 additions & 0 deletions totp_qr_backup/qrscan.py
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)

0 comments on commit 5b11e8b

Please # to comment.