Skip to content

Commit 562a197

Browse files
AHarmlessPyroclaude
andcommitted
Read TOTP seeds split across lines, as vm-service now does
#1573's vm-service drops CR and LF from a seed with its spaces and hyphens before the length check (its base32 decoder skips them), so a seed pasted across lines is valid. The clients refused it; they now drop them too. A raw line break inside an otpauth URI is still refused, as Go's url.Parse does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
1 parent 958618d commit 562a197

3 files changed

Lines changed: 9 additions & 5 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ Things to know:
172172
metadata member sent replaces the stored one and `None` removes it.
173173
- Secret types are `secret` (the default), `totp` and `op_token`, fixed at
174174
creation; `client.secrets.list({"type": "totp"})` filters by type. TOTP
175-
values are checked locally before upload: a base32 seed (spaces, hyphens,
175+
values are checked locally before upload: a base32 seed (spaces, hyphens, line breaks,
176176
lowercase and trailing `=` are fine) or an `otpauth://totp/` URI with
177177
`algorithm` SHA1/SHA256/SHA512, `digits` 6 or 8 and `period` 1-300. Seeds
178178
shorter than 10 bytes (16 characters) are accepted but weak. The platform

‎hyperbrowser/secrets_common.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,13 @@ def _invalid_totp(reason: str) -> HyperbrowserError:
122122

123123

124124
def _check_totp_base32(seed: str) -> None:
125-
# Spaces, hyphens, lowercase and trailing "=" padding are accepted, as
126-
# sites show seeds in all of these forms. Characters are uppercased one
125+
# Spaces, hyphens, line breaks, lowercase and trailing "=" padding are
126+
# accepted, as sites show seeds in all of these forms and the platform
127+
# skips them too. Characters are uppercased one
127128
# at a time, as Go does, so "ß" can't turn into "SS".
128129
normalized = "".join(
129130
upper if len(upper) == 1 else char
130-
for char in seed.replace(" ", "").replace("-", "")
131+
for char in re.sub(r"[ \r\n-]", "", seed)
131132
for upper in [char.upper()]
132133
).rstrip("=")
133134
if not normalized:

‎tests/test_credentials.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,10 @@ def test_totp_secrets_seal_the_seed_under_the_returned_context(private_key):
198198
f"otpauth://totp/x?secret={SEED}&digits=6%zz",
199199
"\ufeff\u3000" + SEED + "\u00a0\u0085",
200200
"\u0131" + SEED[1:],
201+
# The platform skips line breaks, as Go's base32 decoder does.
202+
SEED[:13] + "\n" + SEED[13:],
203+
SEED[:16] + "\r\n" + SEED[16:],
204+
f"otpauth://totp/x?secret={SEED[:16]}%0A{SEED[16:]}",
201205
]
202206
BAD_SEEDS = [
203207
("", "empty"),
@@ -236,7 +240,6 @@ def test_totp_secrets_seal_the_seed_under_the_returned_context(private_key):
236240
("\x1c" + SEED, "not base32"),
237241
(SEED[:-1] + "\u00df", "not base32"),
238242
("\ufb00AAAAAA", "not base32"),
239-
("JBSWY3DP\nEHPK3PXP", "not base32"),
240243
]
241244

242245

0 commit comments

Comments
 (0)