Skip to content

SOPs

TOTP: How the One-Time Code System Works

1 · A service turns on 2FA
OpenAI, Google, GitHub — the "Connect your authenticator app" dialog
↓
2 · It shows the secret in one of two costumes
a QR code or a text string ("Trouble scanning" / "Copy code").
Same secret. The QR is just the text wearing a picture.
↓
3 · Capture it on the way in
Best: click Copy code and grab the text.
Or: screenshot the full QR and run zbarimg on it.
↓
4 · The NAME and the SEED go up to AWS — nothing else
The name is the parameter path: /totp/<Issuer>/<user>.
The seed is the SecureString value stored at that path.
Name = how I find it later. Seed = what I find.
↓
5 · Codes are computed, never stored
HMAC-SHA1(seed, floor(now / 30)) → 6 digits.
The service runs the same math with the same seed — that is the whole trick.

Both sides share the seed once, at setup. After that nothing is ever transmitted: my machine and their server each hash the seed against the clock every 30 seconds and independently arrive at the same 6 digits.

Decode any setup QR and you get an otpauth:// URL:

otpauth://totp/OpenAI:ojhurst@gmail.com?secret=BASE32SEEDGOESHERE&issuer=OpenAI

Everything is right there: the issuer, the account, and secret= — the seed itself. The two views of the same dialog prove it:

The QR costumeThe text costume
OpenAI 2FA dialog showing the QR code (cropped so the QR is incomplete)OpenAI 2FA dialog showing the secret as text (cropped so the secret is incomplete)

So on the way in — while enrolling — I never need a phone. Click “Trouble scanning” or “Copy code” to get the raw secret, or decode the QR locally:

Terminal window
zbarimg --raw -q screenshot-of-qr.png

Two things, packed into one parameter: the name and the seed. The name (issuer + account) becomes the parameter path — that is how I find it next time. The seed becomes the SecureString value stored at that path — that is what I find. Both come straight out of the otpauth:// URL: issuer and the account label form the path, secret= is the value.

ArtifactGoes to AWS?Why
QR code imageNoIt IS the seed in costume. Decode it, then delete the screenshot.
otpauth:// URLNoContains everything as one string. Split it: name → path, secret → value, discard the rest.
The name (issuer + account)Yes — as the parameter path/totp/OpenAI/ojhurst-at-gmail.com. Without it the seed is an anonymous string I can never match to a login.
The seed (base32 string)Yes — as the SecureString valueThe one durable secret. Everything regenerates from it, forever.
6-digit codesNoDerived on demand, worthless after 30 seconds. Storing one is storing a puff of smoke.

Storage convention — one parameter per account, SecureString:

Terminal window
aws ssm put-parameter --name "/totp/OpenAI/ojhurst-at-gmail.com" \
--value "$SEED" --type SecureString --overwrite --profile claude-creds-writer

Read one back (never echo it to a transcript):

Terminal window
SEED=$(~/apps/cc/bin/aws-secret /totp/OpenAI/ojhurst-at-gmail.com)

List what exists:

Terminal window
aws ssm get-parameters-by-path --path /totp --recursive \
--profile claude-creds-reader --query 'Parameters[].Name' --output text

The generator lives in ~/apps/agent-totp/:

Terminal window
~/apps/cc/bin/aws-secret /totp/OpenAI/ojhurst-at-gmail.com | \
python3 ~/apps/agent-totp/generate-code.py --remaining

--remaining shows how many seconds until the code rolls over — useful when an automation needs the code to survive a form submit. The math is three lines of standard library; pyotp just wraps it. Any machine with the seed and a correct clock produces correct codes.

The phone app holds nothing magic — just a list of seeds. “Transfer accounts → Export accounts” packs them into QR codes, roughly 10 accounts per page (“1 of 2”, “2 of 2”). Each page is a protobuf blob containing every account’s name, issuer, and seed.

Terminal window
python3 ~/apps/agent-totp/decode-export.py page1.png page2.png

That outputs JSON with every seed, ready to store at /totp/<Issuer>/<user>. decode-export.py handles both the protobuf export format and plain otpauth:// setup QRs. Full walkthrough: the README in ~/apps/agent-totp/.

  • The setup dialog mints a fresh secret every time it opens. Enroll with the last secret shown, not one screenshotted earlier. Precedent: 2026-08-28, re-enrolling OpenAI — the QR screenshotted at 2:28 PM and the text view at 2:59 PM were two different secrets. The first was already dead.
  • Cropped screenshots do not decode. zbarimg needs the complete QR square. A half-QR is unrecoverable.
  • QR screenshots are secrets. Once the seed is decoded and stored, delete the screenshot — it holds the same seed in scannable form.
  • Codes depend on the clock. A machine minutes off NTP generates wrong codes that look right.
  • Generator + decoder repo: ~/apps/agent-totp/ (its README is the operational setup SOP)
  • Handling Secrets — the general rules for keeping values out of transcripts and git
  • Parameter Store read/write helpers: ~/apps/cc/bin/aws-secret, ~/apps/cc/bin/ask-secret-aws.sh