From 057fb797fd8766cea5fd01fe71d3112fced5eae3 Mon Sep 17 00:00:00 2001 From: CloakHQ Date: Sat, 8 Aug 2026 19:38:03 +0200 Subject: [PATCH] fix(humanize): shiftKey modifier on Shift-punctuation IME events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shift-punctuation (?!:) dispatched its CDP key events without the Shift modifier, so the DOM reported shiftKey=false while Shift was held — differing from a real capture. Pass modifiers=8 on every event dispatched while Shift is down (Shift keydown, the Process keydown, and the keyups that precede the Shift keyup), and 0 at/after the Shift keyup, for both release orders. Python (sync+async) + JS (Playwright+Puppeteer), tests added, verified live. --- cloakbrowser/human/keyboard.py | 30 ++++++++++++++++++---------- cloakbrowser/human/keyboard_async.py | 17 ++++++++-------- js/src/human-puppeteer/keyboard.ts | 17 ++++++++-------- js/src/human/keyboard.ts | 19 ++++++++++-------- js/tests/humanize.test.ts | 16 +++++++++++++++ tests/test_stealth_unit.py | 19 ++++++++++++++++++ 6 files changed, 83 insertions(+), 35 deletions(-) diff --git a/cloakbrowser/human/keyboard.py b/cloakbrowser/human/keyboard.py index 57fd30d..bd33c4a 100644 --- a/cloakbrowser/human/keyboard.py +++ b/cloakbrowser/human/keyboard.py @@ -156,8 +156,11 @@ def _compose_display(typed: list[tuple[int, str]]) -> str: return "'".join(parts[si] for si in order) -def _key_event(type_: str, vk: int, key: str, code: str) -> dict: - return {"type": type_, "windowsVirtualKeyCode": vk, "key": key, "code": code} +def _key_event(type_: str, vk: int, key: str, code: str, modifiers: int = 0) -> dict: + ev = {"type": type_, "windowsVirtualKeyCode": vk, "key": key, "code": code} + if modifiers: + ev["modifiers"] = modifiers # 8 = Shift, so DOM events report shiftKey=true + return ev def _type_cjk_phrase( @@ -225,30 +228,35 @@ def _type_cjk_punct(ch: str, cfg: HumanConfig, cdp_session: Any) -> None: Microsoft Pinyin capture: keydown Process/229 on its physical key → composition → commit (compositionend, untrusted) → dual keyup (229 + physical keyCode). Shift marks wrap it in Shift; the Shift-release order is randomized (real users vary it), - and the keyup `key` char follows the Shift state at release.""" + and the keyup `key` char follows the Shift state at release. Every event dispatched + while Shift is held carries modifiers=8 so the DOM reports shiftKey=true, matching a + real capture; events at/after the Shift keyup carry 0.""" shift, code, kc, base_key, shifted_key = _CJK_PUNCT[ch] + held = 8 if shift else 0 # Shift modifier flag while Shift is down if shift: cdp_session.send("Input.dispatchKeyEvent", - _key_event("keyDown", 16, "Shift", "ShiftLeft")) + _key_event("keyDown", 16, "Shift", "ShiftLeft", 8)) sleep_ms(rand_range(cfg.shift_down_delay)) - cdp_session.send("Input.dispatchKeyEvent", _key_event("keyDown", 229, "Process", code)) + cdp_session.send("Input.dispatchKeyEvent", _key_event("keyDown", 229, "Process", code, held)) cdp_session.send("Input.imeSetComposition", {"text": ch, "selectionStart": len(ch), "selectionEnd": len(ch)}) sleep_ms(rand_range(cfg.key_hold)) cdp_session.send("Input.insertText", {"text": ch}) # commit → compositionend release_shift_first = shift and random.random() < 0.5 if release_shift_first: + # Shift released first → subsequent keyups are already shiftKey=false. cdp_session.send("Input.dispatchKeyEvent", - _key_event("keyUp", 16, "Shift", "ShiftLeft")) - cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", 229, "Process", code)) - cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", kc, base_key, code)) + _key_event("keyUp", 16, "Shift", "ShiftLeft", 0)) + cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", 229, "Process", code, 0)) + cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", kc, base_key, code, 0)) else: - cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", 229, "Process", code)) + # Shift still held → these keyups are shiftKey=true; Shift keyup last is false. + cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", 229, "Process", code, held)) cdp_session.send("Input.dispatchKeyEvent", - _key_event("keyUp", kc, shifted_key if shift else base_key, code)) + _key_event("keyUp", kc, shifted_key if shift else base_key, code, held)) if shift: cdp_session.send("Input.dispatchKeyEvent", - _key_event("keyUp", 16, "Shift", "ShiftLeft")) + _key_event("keyUp", 16, "Shift", "ShiftLeft", 0)) def human_type( diff --git a/cloakbrowser/human/keyboard_async.py b/cloakbrowser/human/keyboard_async.py index ded1dc7..19a4545 100644 --- a/cloakbrowser/human/keyboard_async.py +++ b/cloakbrowser/human/keyboard_async.py @@ -64,11 +64,12 @@ async def _type_cjk_phrase( async def _type_cjk_punct(ch: str, cfg: HumanConfig, cdp_session: Any) -> None: """Async mirror of keyboard._type_cjk_punct — full-width punctuation via the IME.""" shift, code, kc, base_key, shifted_key = _CJK_PUNCT[ch] + held = 8 if shift else 0 # Shift modifier flag while Shift is down if shift: await cdp_session.send("Input.dispatchKeyEvent", - _key_event("keyDown", 16, "Shift", "ShiftLeft")) + _key_event("keyDown", 16, "Shift", "ShiftLeft", 8)) await async_sleep_ms(rand_range(cfg.shift_down_delay)) - await cdp_session.send("Input.dispatchKeyEvent", _key_event("keyDown", 229, "Process", code)) + await cdp_session.send("Input.dispatchKeyEvent", _key_event("keyDown", 229, "Process", code, held)) await cdp_session.send("Input.imeSetComposition", {"text": ch, "selectionStart": len(ch), "selectionEnd": len(ch)}) await async_sleep_ms(rand_range(cfg.key_hold)) @@ -76,16 +77,16 @@ async def _type_cjk_punct(ch: str, cfg: HumanConfig, cdp_session: Any) -> None: release_shift_first = shift and random.random() < 0.5 if release_shift_first: await cdp_session.send("Input.dispatchKeyEvent", - _key_event("keyUp", 16, "Shift", "ShiftLeft")) - await cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", 229, "Process", code)) - await cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", kc, base_key, code)) + _key_event("keyUp", 16, "Shift", "ShiftLeft", 0)) + await cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", 229, "Process", code, 0)) + await cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", kc, base_key, code, 0)) else: - await cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", 229, "Process", code)) + await cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", 229, "Process", code, held)) await cdp_session.send("Input.dispatchKeyEvent", - _key_event("keyUp", kc, shifted_key if shift else base_key, code)) + _key_event("keyUp", kc, shifted_key if shift else base_key, code, held)) if shift: await cdp_session.send("Input.dispatchKeyEvent", - _key_event("keyUp", 16, "Shift", "ShiftLeft")) + _key_event("keyUp", 16, "Shift", "ShiftLeft", 0)) async def async_human_type( diff --git a/js/src/human-puppeteer/keyboard.ts b/js/src/human-puppeteer/keyboard.ts index a70a839..f101fa3 100644 --- a/js/src/human-puppeteer/keyboard.ts +++ b/js/src/human-puppeteer/keyboard.ts @@ -20,23 +20,24 @@ import { LETTER_CODES, buildPhrases, composeDisplay, letterKeycode, CJK_PUNCT } * Playwright keyboard.ts typeCjkPunct for the model. */ async function typeCjkPunct(ch: string, cfg: HumanConfig, cdpSession: CDPSession): Promise { const [shift, code, kc, baseKey, shiftedKey] = CJK_PUNCT[ch]; + const held = shift ? 8 : 0; // Shift modifier while Shift is held → shiftKey=true if (shift) { - await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyDown', windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' }); + await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyDown', modifiers: 8, windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' }); await sleep(randRange(cfg.shift_down_delay)); } - await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyDown', windowsVirtualKeyCode: 229, key: 'Process', code }); + await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyDown', modifiers: held, windowsVirtualKeyCode: 229, key: 'Process', code }); await cdpSession.send('Input.imeSetComposition', { text: ch, selectionStart: ch.length, selectionEnd: ch.length }); await sleep(randRange(cfg.key_hold)); await cdpSession.send('Input.insertText', { text: ch }); const releaseShiftFirst = shift && Math.random() < 0.5; if (releaseShiftFirst) { - await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' }); - await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: 229, key: 'Process', code }); - await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: kc, key: baseKey, code }); + await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: 0, windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' }); + await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: 0, windowsVirtualKeyCode: 229, key: 'Process', code }); + await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: 0, windowsVirtualKeyCode: kc, key: baseKey, code }); } else { - await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: 229, key: 'Process', code }); - await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: kc, key: shift ? shiftedKey : baseKey, code }); - if (shift) await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' }); + await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: held, windowsVirtualKeyCode: 229, key: 'Process', code }); + await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: held, windowsVirtualKeyCode: kc, key: shift ? shiftedKey : baseKey, code }); + if (shift) await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: 0, windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' }); } } diff --git a/js/src/human/keyboard.ts b/js/src/human/keyboard.ts index e1215a5..9850674 100644 --- a/js/src/human/keyboard.ts +++ b/js/src/human/keyboard.ts @@ -241,23 +241,26 @@ export async function typeCjkPunct( cdpSession: CDPSession, ): Promise { const [shift, code, kc, baseKey, shiftedKey] = CJK_PUNCT[ch]; + // modifiers=8 (Shift) on every event dispatched while Shift is held, so the DOM + // reports shiftKey=true, matching a real capture; 0 at/after the Shift keyup. + const held = shift ? 8 : 0; if (shift) { - await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyDown', windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' }); + await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyDown', modifiers: 8, windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' }); await sleep(randRange(cfg.shift_down_delay)); } - await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyDown', windowsVirtualKeyCode: 229, key: 'Process', code }); + await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyDown', modifiers: held, windowsVirtualKeyCode: 229, key: 'Process', code }); await cdpSession.send('Input.imeSetComposition', { text: ch, selectionStart: ch.length, selectionEnd: ch.length }); await sleep(randRange(cfg.key_hold)); await cdpSession.send('Input.insertText', { text: ch }); const releaseShiftFirst = shift && Math.random() < 0.5; if (releaseShiftFirst) { - await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' }); - await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: 229, key: 'Process', code }); - await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: kc, key: baseKey, code }); + await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: 0, windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' }); + await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: 0, windowsVirtualKeyCode: 229, key: 'Process', code }); + await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: 0, windowsVirtualKeyCode: kc, key: baseKey, code }); } else { - await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: 229, key: 'Process', code }); - await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: kc, key: shift ? shiftedKey : baseKey, code }); - if (shift) await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' }); + await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: held, windowsVirtualKeyCode: 229, key: 'Process', code }); + await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: held, windowsVirtualKeyCode: kc, key: shift ? shiftedKey : baseKey, code }); + if (shift) await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: 0, windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' }); } } diff --git a/js/tests/humanize.test.ts b/js/tests/humanize.test.ts index 9a9ccaa..8e2c7f3 100644 --- a/js/tests/humanize.test.ts +++ b/js/tests/humanize.test.ts @@ -1864,6 +1864,22 @@ describe("CJK pinyin-IME humanize (ime_language='zh')", () => { expect(kd).toContainEqual([229, "Slash"]); expect(calls).toContainEqual(["Input.insertText", { text: "?" }]); expect(raw.insertText).not.toHaveBeenCalled(); + // Shift modifier: events while Shift is held carry modifiers=8, then 0 after keyup. + const evs = calls.filter(([m]) => m === "Input.dispatchKeyEvent").map(([, p]) => p); + const shiftUp = evs.findIndex(p => p.type === "keyUp" && p.code === "ShiftLeft"); + evs.forEach((p, i) => { + expect(p.modifiers).toBe(i < shiftUp ? 8 : 0); + }); + }); + + it("non-shift punctuation (,) carries no Shift modifier", async () => { + const { humanType } = await import("../src/human/keyboard.js"); + const cfg = resolveConfig("default", { ime_language: "zh", mistype_chance: 0 }); + const raw = mockRaw(); + const { cdp, calls } = mockCdp(); + await humanType({} as any, raw as any, ",", cfg, cdp); + const evs = calls.filter(([m]) => m === "Input.dispatchKeyEvent").map(([, p]) => p); + evs.forEach(p => expect(p.modifiers ?? 0).toBe(0)); }); it("punctuation falls back to insertText when disabled", async () => { diff --git a/tests/test_stealth_unit.py b/tests/test_stealth_unit.py index 992e78a..a43f302 100644 --- a/tests/test_stealth_unit.py +++ b/tests/test_stealth_unit.py @@ -1706,6 +1706,25 @@ class TestCJKPunctuation: seen_last = True assert seen_first and seen_last # both orderings occur + def test_shift_modifier_tracks_shift_state(self): + # Every event while Shift is held carries modifiers=8 (shiftKey=true); the + # Shift keyup and everything after carry 0. Holds for both release orders. + for _ in range(30): + _, calls = self._run("!") + evs = [p for m, p in calls if m == "Input.dispatchKeyEvent"] + shift_up = next(i for i, p in enumerate(evs) + if p["type"] == "keyUp" and p["code"] == "ShiftLeft") + for i, p in enumerate(evs): + if i < shift_up: + assert p.get("modifiers") == 8, f"{p} should be shiftKey=true" + else: + assert p.get("modifiers", 0) == 0, f"{p} should be shiftKey=false" + + def test_non_shift_punct_has_no_modifier(self): + _, calls = self._run(",") + evs = [p for m, p in calls if m == "Input.dispatchKeyEvent"] + assert all("modifiers" not in p for p in evs) + def test_disabled_and_no_cdp_fall_back(self): raw, calls = self._run(",", ime=None) raw.insert_text.assert_called_once_with(",")