otpauth:// URIs accepted and produced by feuristic¶
Status: normative for feuristic v0.x. Implementation:
src/feuristic/apps/otp/uri.py (codec), src/feuristic/core/base32.py,
src/feuristic/apps/otp/hotp.py, totp.py and accounts.py. Product
requirements: spec F01, §3.3, §8.1.
feuristic imports OTP accounts from QR codes that contain an otpauth://
URI in the Google Authenticator Key Uri Format. It also shows such a QR to
export a single account. This document lists exactly which subset is
accepted, with its limits.
1. Syntax¶
| Part | Rule |
|---|---|
| scheme | otpauth, case-insensitive |
TYPE |
totp or hotp, case-insensitive. Anything else (steam, motp, …) is refused |
LABEL |
account or issuer:account. The colon may be literal or %3A; spaces after it are ignored |
PARAMETERS |
name=value pairs separated by &; parameter names are case-insensitive |
1.1 Parameters¶
| Name | Required | Accepted values | Default |
|---|---|---|---|
secret |
yes | base32 (RFC 4648), see §2 | — |
issuer |
no | text, see §3 | label issuer |
algorithm |
no | SHA1, SHA256, SHA512, case-insensitive |
SHA1 |
digits |
no | 6, 7, 8 |
6 |
period |
no (TOTP) | integer 15 … 300 (seconds) | 30 |
counter |
yes for HOTP | integer 0 … 2⁶⁴−1 | — |
- Unknown parameters (
image,color,lock, …) are ignored. periodis ignored for HOTP andcounteris ignored for TOTP.- A known parameter that appears twice (in any letter case) makes the URI
invalid. So does a known parameter without
=. - Empty parts (
&&, a trailing&) are ignored. An emptyissuer=counts as absent; any other empty known parameter is an error. - Numbers are ASCII decimal digits only: no sign, no spaces, no hexadecimal, at most 20 digits.
2. Secret¶
- Decoded with the RFC 4648 §6 base32 alphabet, case-insensitive. Spaces
and hyphens are ignored, so
JBSW Y3DPandjbsw-y3dpboth work. - Padding
=(or%3D) is optional. If present, it must be at the end and complete. - As in the reference Google Authenticator decoder, characters are consumed 5 bits at a time and incomplete trailing bits are dropped. This accepts the "random base32 characters" secrets of any length that many services generate. The key bytes are the same ones other authenticators use, so the codes match.
- Decoded length: 1 to 64 bytes. A secret shorter than 10 bytes (80 bits) is accepted but flagged as weak: the add-account screen shows a warning. RFC 4226 §4 R6 requires 128 bits and recommends 160.
3. Issuer and account¶
- Percent-decoding follows RFC 3986 §2.1. Malformed escapes (
%,%4,%G1) are errors. - In query values
+decodes to a space, as in HTML form encoding (issuer=ACME+Co). In the label+is a literal character (a+b@example.com). - Decoded text must be valid UTF-8 (RFC 3629: no overlong forms, no surrogates, nothing above U+10FFFF).
- Refused characters, to prevent display spoofing: C0 and C1 controls, DEL, U+061C, U+200B–U+200F, U+2028–U+202E, U+2060–U+206F and U+FEFF. This covers zero-width characters and bidirectional overrides.
- Leading and trailing spaces are removed. Neither name may contain
:. - Limits, in UTF-8 bytes after decoding (docs/architecture.md §6.3): issuer ≤ 32 B, account 1 … 64 B.
- If the label has an issuer prefix and the
issuerparameter is set, the two must be identical (case-sensitive). Otherwise the URI is refused. If only one is present, it is used.
4. Whole-URI limits¶
- At most 512 bytes, checked before any other parsing.
- Raw (not percent-encoded) UTF-8 and raw spaces are tolerated, because some generators produce them. Raw control characters are not.
- A fragment (
#…) is refused instead of being silently dropped.
5. Examples¶
Accepted:
otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30
otpauth://hotp/Corp:bob?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&counter=42&digits=8&algorithm=sha256
otpauth://totp/Soci%C3%A9t%C3%A9:j%C3%BCrgen?secret=jbsw-y3dp-ehpk-3pxp&image=https%3A%2F%2Fexample.com%2Flogo.png
Refused (reason):
otpauth://totp/A:me?secret=JBSWY3DPEHPK3PXP&issuer=B issuer mismatch
otpauth://hotp/me?secret=JBSWY3DPEHPK3PXP HOTP without counter
otpauth://totp/me?secret=JBSWY3DPEHPK3PXP&digits=10 digits not in 6..8
otpauth://totp/me?secret=JBSWY3DPEHPK3PX1 '1' is not base32
otpauth://totp/me?secret=JBSWY3DPEHPK3PXP&period=0 period not in 15..300
otpauth://totp/Bank%E2%80%AE:me?secret=JBSWY3DPEHPK3PXP bidirectional override
otpauth://totp/me?secret=A&secret=B duplicate parameter
6. Import behaviour (device)¶
| Situation | Device behaviour |
|---|---|
| New issuer + account | confirmation screen (service, account, type, algorithm, digits, period or counter; weak-secret warning if needed) |
| Same issuer + account and same secret | "This account already exists", nothing is written |
| Same issuer + account, different secret | DANGER confirmation "Replace the account?". The old secret is erased if confirmed |
| 32 accounts already stored | "Storage full", nothing is written |
Replacing an HOTP account with the same secret (only possible through a backup restore) never moves its counter backwards.
7. Export format (spec §8.1)¶
The device shows one account as a QR code containing a canonical URI:
otpauth://TYPE/[ISSUER:]ACCOUNT?secret=SECRET[&issuer=ISSUER]&algorithm=ALG&digits=D&(period=P|counter=C)
SECRET: upper-case base32, without padding.ISSUER/ACCOUNT: UTF-8, every byte other thanA–Z a–z 0–9 - . _ ~ @percent-encoded as%XX(upper-case hex). The label colon is literal.algorithm,digitsandperiod/counterare always present. Parameter order is fixed:secret,issuer,algorithm,digits,period|counter.- HOTP
counteris the next unused moving factor. An authenticator that imports the QR continues without reusing a value the device already showed. The device keeps its own counter, so codes from both may collide if both are used afterwards. Exporting is meant for migration. - If the URI would exceed 512 bytes (only possible with long non-ASCII
names), the issuer prefix of the label is left out. The
issuerparameter, which every reader prefers, stays.
Exporting reveals the secret. The firmware requires the device PIN again, a hold-to-confirm DANGER screen warning that anyone who photographs this QR code can reproduce your codes, shows the QR only on the device screen (never over USB), and hides it automatically after 60 seconds.