From f98cb00a8beb7f8c8a305d7c377c9c5f777c85ae Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:32:58 -0700 Subject: [PATCH] =?UTF-8?q?fix(vault):=202FA=20review=20follow-ups=20?= =?UTF-8?q?=E2=80=94=20per-digit=20fill=20only=20for=20an=20unmistakable?= =?UTF-8?q?=20maxlength=3D1=20widget;=20honour=20otpauth=20digits/period/a?= =?UTF-8?q?lgorithm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer findings on #107585: - build_otp_fills split any >=4 code-like controls into digits. A page with promo/zip/referral 'code' inputs next to the real OTP box would have had a digit sprayed across unrelated fields. Split now requires exactly len(code) controls that are all maxlength=1, same form, adjacent in DOM order (inspection JS exports maxLength); anything else fills ONE field, the best-scoring one. Verified on the real Browser Use stack: 6-box widget gets one digit each; scattered page fills only the one-time-code input. - normalize_otp_secret dropped digits/period/algorithm from otpauth:// URIs, so an 8-digit or SHA-256 authenticator would get wrong codes. Non-default parameters are now stored as seed|digits|period|algo and honoured (RFC 6238 SHA-256 8-digit vector added); hotp:// is rejected explicitly. - rebase on main (prompts.ts conflict) + prettier. --- .../mock._session_db.db_path/126402682339024 | Bin 0 -> 262144 bytes .../126402682339024.fts_rebuild.lock | 0 .../126402682339024.quarantine.lock | 0 .../mock._session_db.db_path/126402702293264 | Bin 0 -> 262144 bytes .../126402702293264.fts_rebuild.lock | 0 .../126402702293264.quarantine.lock | 0 agent/vault_login_classifier.py | 20 ++++++--- agent/vault_store.py | 39 ++++++++++++++---- apps/desktop/src/store/prompts.ts | 10 ++++- tests/test_browser_vault.py | 29 ++++++++++++- 10 files changed, 83 insertions(+), 15 deletions(-) create mode 100644 MagicMock/mock._session_db.db_path/126402682339024 create mode 100644 MagicMock/mock._session_db.db_path/126402682339024.fts_rebuild.lock create mode 100644 MagicMock/mock._session_db.db_path/126402682339024.quarantine.lock create mode 100644 MagicMock/mock._session_db.db_path/126402702293264 create mode 100644 MagicMock/mock._session_db.db_path/126402702293264.fts_rebuild.lock create mode 100644 MagicMock/mock._session_db.db_path/126402702293264.quarantine.lock diff --git a/MagicMock/mock._session_db.db_path/126402682339024 b/MagicMock/mock._session_db.db_path/126402682339024 new file mode 100644 index 0000000000000000000000000000000000000000..2cc4d1be357b5fb07227d4cbab799b043d5d6a82 GIT binary patch literal 262144 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 literal 0 HcmV?d00001 diff --git a/MagicMock/mock._session_db.db_path/126402702293264.fts_rebuild.lock b/MagicMock/mock._session_db.db_path/126402702293264.fts_rebuild.lock new file mode 100644 index 0000000000..e69de29bb2 diff --git a/MagicMock/mock._session_db.db_path/126402702293264.quarantine.lock b/MagicMock/mock._session_db.db_path/126402702293264.quarantine.lock new file mode 100644 index 0000000000..e69de29bb2 diff --git a/agent/vault_login_classifier.py b/agent/vault_login_classifier.py index 690a8175ad..684f113b45 100644 --- a/agent/vault_login_classifier.py +++ b/agent/vault_login_classifier.py @@ -72,10 +72,12 @@ class LoginControl: label: str name: str type: str + max_length: Optional[int] = None @classmethod def from_dict(cls, raw: Dict[str, Any]) -> "LoginControl": form_index = raw.get("formIndex", raw.get("form_index")) + max_length = raw.get("maxLength", raw.get("max_length")) return cls( autocomplete=str(raw.get("autocomplete") or ""), form_index=int(form_index) if form_index is not None else None, @@ -83,6 +85,7 @@ class LoginControl: label=str(raw.get("label") or ""), name=str(raw.get("name") or ""), type=str(raw.get("type") or ""), + max_length=int(max_length) if max_length is not None else None, ) @@ -223,12 +226,18 @@ INSPECTION_STAMP_ATTR = "data-hermes-vault-slot" def build_otp_fills(otp_controls: List[ClassifiedLoginControl], code: str) -> List[Dict[str, Any]]: - """One fill per box: a single input takes the whole code; N single-char boxes (maxlength=1 pattern, - detected as N>=4 same-form OTP controls) each take one digit in DOM order.""" - boxes = sorted(otp_controls, key=lambda c: c.control.index) - if len(boxes) >= 4 and len(boxes) <= len(code): + """One fill per box. Default: the single best-scoring code field takes the whole code. + + Per-digit entry only when the page unmistakably uses it: exactly len(code) OTP controls that are all + ``maxlength=1``, all in the same form, and adjacent in DOM order (the classic N-box widget). Anything + looser (several code-like inputs scattered over a page) gets ONE field, never a digit sprayed across + unrelated inputs.""" + best = max(otp_controls, key=lambda c: c.score) + boxes = sorted((c for c in otp_controls if c.control.max_length == 1), key=lambda c: c.control.index) + if (len(boxes) == len(code) + and len({b.control.form_index for b in boxes}) == 1 + and all(b.control.index - a.control.index == 1 for a, b in zip(boxes, boxes[1:]))): return [{"index": b.control.index, "token": "one-time-code", "value": ch} for b, ch in zip(boxes, code)] - best = max(boxes, key=lambda c: c.score) return [{"index": best.control.index, "token": "one-time-code", "value": code}] @@ -256,6 +265,7 @@ _LOGIN_CONTROL_INSPECTION_JS_TEMPLATE = """(() => { autocomplete: element.autocomplete || "", formIndex: resolvedFormIndex >= 0 ? resolvedFormIndex : null, index, + maxLength: element.maxLength > 0 ? element.maxLength : null, label: [ ...labels, element.getAttribute("aria-label") || "", diff --git a/agent/vault_store.py b/agent/vault_store.py index f69db81ff0..5eb3d934d9 100644 --- a/agent/vault_store.py +++ b/agent/vault_store.py @@ -67,34 +67,57 @@ class VaultError(Exception): """Vault failure that is safe to surface (never contains secret values).""" +_OTP_ALGOS = {"SHA1": "sha1", "SHA256": "sha256", "SHA512": "sha512"} + + def normalize_otp_secret(value: str) -> str: - """Accept a raw base32 seed or an ``otpauth://totp/...?secret=...`` URI; return the bare base32 seed - (uppercase, no spaces) or "" when empty/unusable. Only the seed is stored; issuer/digits/period use - RFC 6238 defaults, which every mainstream site uses.""" + """Accept a raw base32 seed or an ``otpauth://totp/...`` URI. Returns the canonical stored form: + the bare uppercase base32 seed, followed by ``|digits|period|algo`` ONLY when the URI departs from + the RFC 6238 defaults (6 / 30 / SHA1), so a plain seed stays a plain seed. Non-default parameters + are honoured, not dropped: an 8-digit or 60-second authenticator would otherwise get wrong codes.""" value = (value or "").strip() if not value: return "" + digits, period, algo = 6, 30, "SHA1" if value.lower().startswith("otpauth://"): from urllib.parse import parse_qs, urlparse - qs = parse_qs(urlparse(value).query) - value = (qs.get("secret") or [""])[0] + parsed = urlparse(value) + if parsed.netloc.lower() != "totp": + raise VaultError("only otpauth://totp links are supported (counter-based HOTP is not)") + qs = {k.lower(): v[0] for k, v in parse_qs(parsed.query).items()} + value = qs.get("secret", "") + try: + digits = int(qs.get("digits", digits)) + period = int(qs.get("period", period)) + except ValueError: + raise VaultError("otpauth:// digits/period must be integers") + algo = qs.get("algorithm", algo).upper().replace("-", "") + if digits not in (6, 7, 8) or period <= 0 or algo not in _OTP_ALGOS: + raise VaultError("unsupported otpauth:// parameters (digits 6-8, period > 0, SHA1/SHA256/SHA512)") seed = re.sub(r"[\s-]", "", value).upper().rstrip("=") if not seed or re.search(r"[^A-Z2-7]", seed): raise VaultError("authenticator key must be a base32 secret or an otpauth:// URI") - return seed + if (digits, period, algo) == (6, 30, "SHA1"): + return seed + return f"{seed}|{digits}|{period}|{algo}" def totp_now(seed: str, *, digits: int = 6, period: int = 30, at: Optional[float] = None) -> str: - """RFC 6238 TOTP (SHA-1) for a base32 seed. Stdlib only: no dependency for six digits.""" + """RFC 6238 TOTP for a stored seed (see normalize_otp_secret for the ``seed|digits|period|algo`` + form). Stdlib only.""" import base64 import hashlib import hmac import struct import time as _time + algo = "sha1" + if "|" in seed: + seed, d, p, a = seed.split("|", 3) + digits, period, algo = int(d), int(p), _OTP_ALGOS.get(a.upper(), "sha1") key = base64.b32decode(seed + "=" * (-len(seed) % 8), casefold=True) counter = int((at if at is not None else _time.time()) // period) - digest = hmac.new(key, struct.pack(">Q", counter), hashlib.sha1).digest() + digest = hmac.new(key, struct.pack(">Q", counter), getattr(hashlib, algo)).digest() offset = digest[-1] & 0x0F code = (struct.unpack(">I", digest[offset:offset + 4])[0] & 0x7FFFFFFF) % (10 ** digits) return str(code).zfill(digits) diff --git a/apps/desktop/src/store/prompts.ts b/apps/desktop/src/store/prompts.ts index 120d5eed39..d170d0c35a 100644 --- a/apps/desktop/src/store/prompts.ts +++ b/apps/desktop/src/store/prompts.ts @@ -272,7 +272,15 @@ export const sessionVaultCodeRequest = (sessionId: string | null) => // suppress "thinking" indicators and the Esc-to-interrupt shortcut while you // decide, instead of treating the wait as an in-flight turn. export const $activeSessionAwaitingInput = computed( - [$clarifyRequest, $approvalRequest, $sudoRequest, $secretRequest, $vaultUnlockRequest, $vaultSaveLoginRequest, $vaultCodeRequest], + [ + $clarifyRequest, + $approvalRequest, + $sudoRequest, + $secretRequest, + $vaultUnlockRequest, + $vaultSaveLoginRequest, + $vaultCodeRequest + ], (clarify, approval, sudo, secret, vault, save, code) => Boolean(clarify || approval || sudo || secret || vault || save || code) ) diff --git a/tests/test_browser_vault.py b/tests/test_browser_vault.py index 7dfc4066db..4487162e9d 100644 --- a/tests/test_browser_vault.py +++ b/tests/test_browser_vault.py @@ -692,6 +692,14 @@ class TestTwoFactor: assert normalize_otp_secret("otpauth://totp/GitHub:tek?secret=jbsw y3dp ehpk3pxp&issuer=GitHub") == "JBSWY3DPEHPK3PXP" with pytest.raises(VaultError): normalize_otp_secret("not base32!") + # Non-default otpauth parameters are kept and honoured (RFC 6238 SHA-256 / 8-digit vector at T=59). + stored = normalize_otp_secret(f"otpauth://totp/x?secret={'GEZDGNBVGY3TQOJQ' * 2}&digits=8&period=30&algorithm=SHA256") + assert stored.endswith("|8|30|SHA256") + sha256_seed = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA" # "1234567890" * 3.2 -> RFC 32-byte seed + assert totp_now(sha256_seed + "|8|30|SHA256", at=59) == "46119246" + assert totp_now("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ|6|60|SHA1", at=119) == totp_now("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", period=60, at=119) + with pytest.raises(VaultError): + normalize_otp_secret("otpauth://hotp/x?secret=JBSWY3DPEHPK3PXP&counter=1") def test_saved_authenticator_key_mints_codes_without_asking(self, store, monkeypatch): """The whole point: with a seed on the login, enter_code never prompts and the code never comes back.""" @@ -729,7 +737,8 @@ class TestTwoFactor: from tools import browser_vault_tool unlock_mod.set_code_prompt_callback(lambda site, hint: "246 810") - boxes = [{"index": i, "type": "tel", "name": f"digit{i}", "label": "", "autocomplete": "one-time-code"} for i in range(6)] + boxes = [{"index": i, "type": "tel", "name": f"digit{i}", "label": "", "autocomplete": "one-time-code", + "formIndex": 0, "maxLength": 1} for i in range(6)] seen = {} fake_eval = lambda t, e: {"success": True, "result": json.dumps(boxes) if "querySelectorAll" in e else "https://acme.test/2fa"} @@ -749,6 +758,24 @@ class TestTwoFactor: assert re.findall(r'"value": "(\d)"', seen["expr"]) == list("246810") assert declined["error_type"] == "code_declined" + def test_several_code_like_inputs_that_are_not_a_digit_widget_get_one_field(self): + """Reviewer case: a page with 4+ code-ish inputs (promo code, zip code, a real OTP box...) must never + get a digit sprayed across them. Only an unmistakable maxlength=1 same-form adjacent group splits.""" + from agent.vault_login_classifier import ClassifiedLoginControl, LoginControl, build_otp_fills + + def ctl(i, form=0, maxlen=None, score=70): + return ClassifiedLoginControl(LoginControl("", form, i, "", f"code{i}", "text", maxlen), score, "one-time-code") + + scattered = [ctl(0), ctl(3), ctl(7), ctl(9, form=1), ctl(12, score=100)] + assert build_otp_fills(scattered, "246810") == [{"index": 12, "token": "one-time-code", "value": "246810"}] + # maxlength=1 but different forms / non-adjacent: still one field + assert len(build_otp_fills([ctl(i, form=i % 2, maxlen=1) for i in range(6)], "246810")) == 1 + assert len(build_otp_fills([ctl(i * 2, maxlen=1) for i in range(6)], "246810")) == 1 + # five boxes for a six-digit code: one field + assert len(build_otp_fills([ctl(i, maxlen=1) for i in range(5)], "246810")) == 1 + # the real widget + assert [f["value"] for f in build_otp_fills([ctl(i + 4, maxlen=1) for i in range(6)], "246810")] == list("246810") + def test_no_code_field_points_at_passkey_or_device_approval(self): from tools import browser_vault_tool