Add QR-to-CSV utility and switch canonical format to CSV

Add qr_to_sheet.py: decode saved OTP QR screenshots and append new
credentials to the canonical CSV, deduplicated on the otpauth:// data.
Add otpauth.py (parse_otpauth / canonical_key / build_otpauth), shared
by both tools. The canonical format becomes a minimal CSV (issuer,
account, otpauth_uri, source_file); load_credentials reads CSV or xlsx
and derives crypto from the immutable otpauth_uri while taking
issuer/account from their editable label columns.
This commit is contained in:
2026-07-08 14:49:59 -07:00
parent cd705a446b
commit 07f65835d0
7 changed files with 577 additions and 48 deletions

View File

@@ -228,6 +228,24 @@ class TestLoadCredentials(unittest.TestCase):
self.assertEqual(g.source, "g.png")
self.assertEqual((creds[1].algorithm, creds[1].digits, creds[1].period), (HMAC_SHA256, 8, 15))
def test_csv_takes_labels_from_columns_and_crypto_from_otpauth(self):
import csv
d = tempfile.mkdtemp()
path = os.path.join(d, "otp-keys.csv")
with open(path, "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["issuer", "account", "otpauth_uri", "source_file"])
# otpauth label is "Amal Graafstra"; the editable account differs
w.writerow(["Heroku", "amal@vivokey.com",
"otpauth://totp/Heroku:Amal%20Graafstra?secret=JBSWY3DP"
"&issuer=Heroku&algorithm=SHA256&digits=8&period=60", "h.png"])
creds = load_credentials(path)
c = creds[0]
self.assertEqual((c.issuer, c.account), ("Heroku", "amal@vivokey.com")) # from columns
self.assertEqual(c.secret, "JBSWY3DP") # from otpauth
self.assertEqual((c.oath_type, c.algorithm, c.digits, c.period),
(TOTP_TYPE, HMAC_SHA256, 8, 60)) # from otpauth
class TestCardChannel(unittest.TestCase):
def test_reassembles_61xx_via_send_remaining(self):