Skip to content

fix(web_api): sign request body in Hawk auth for B01 /jobs writes#852

Merged
Lash-L merged 1 commit into
Python-roborock:mainfrom
andrewlyeats:fix/b01-jobs-hawk-body-signing
Jun 20, 2026
Merged

fix(web_api): sign request body in Hawk auth for B01 /jobs writes#852
Lash-L merged 1 commit into
Python-roborock:mainfrom
andrewlyeats:fix/b01-jobs-hawk-body-signing

Conversation

@andrewlyeats

Copy link
Copy Markdown
Contributor

Fixes #849.

B01 /jobs writes fail with 401 auth.err.invalid.token. This is the REST path for schedules and any
deferred/cron job — the one place /jobs is the only option (immediate room cleans now have an MQTT route via
#851; scheduling a future clean still has to go through /jobs). _get_hawk_authentication always derives
the Hawk payload slot from formdata, which is empty for a JSON body, so the MAC never covers the body the
server receives. GET and bodyless DELETE /jobs/{id} are unaffected.

The fix is two coupled parts:

  • When a JSON body is signed, put md5(compact-JSON body) in the payload slot.
  • Send those same compact bytes (data=), since json= re-serializes with spaces and breaks the MAC.

With body=None, GET and form-post signing stay byte-identical.

Verified on a Q10 S5+ (roborock.vacuum.ss07): with body signing, POST/PUT /jobs return 200; the current
empty-payload-slot signature returns 401. The official Roborock iOS app sends the body compact (its
Content-Length of 179 equals the no-space serialization, not the longer spaced form), so the exact bytes
must be signed and sent (data=, not json=). The complete PUT /jobs from the iOS app, observed via an
HTTPS proxy — every line in its original order, only sensitive values redacted in place (Authorization,
X-UID, Cookie) and device identifiers placeheld:

PUT /user/devices/{deviceId}/jobs/{jobId}
Host: api-us.roborock.com
X-IOTSDK-VERSION: 3.4.0_5_09
Accept: */*
Authorization: Hawk id=<redacted>,s=<redacted>,ts=1781315205,nonce=<redacted>,mac=<redacted>
X-APP-NAME: com.roborock.smart
X-UID: <redacted>
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US;q=1, es-US;q=0.9, zh-Hans-US;q=0.8
X-APP-VERSION-CODE: 4.64.02
Content-Type: application/json
Content-Length: 179
User-Agent: com.roborock.smart/4.64.02_2 (iPhone14,8; iOS 26.5; Scale/3.00)
Connection: keep-alive
Cookie: <redacted>
{"cron":"05 10 * * ?","repeated":true,"enabled":true,"param":{"mapId":<mapId>,"cleanRoute":2,"fanLevel":3,"cleanMode":1,"rooms":[],"roomCount":0,"waterLevel":2,"cleanCount":2}}

The auth change is generic for any body-bearing write; I've wired one create_job consumer in the existing
get_* style. Happy to adjust naming or scope in review.

This is my first PR; AI-assisted, reviewed and refined with care by me: Andrew, a human.

B01 /jobs writes (schedules, one-time room cleans) return 401
auth.err.invalid.token: _get_hawk_authentication only ever signs formdata in
the Hawk payload slot, which is empty for a JSON body, so the MAC never covers
the body the server receives.

Sign md5(compact JSON body) in the payload slot for body-bearing writes, and
send those same compact bytes via data= (json= would re-serialize with spaces
and break the MAC). body=None keeps GET and form-post signing byte-identical.

Add a create_job consumer in the existing get_* style.

Fixes Python-roborock#849

@Lash-L Lash-L left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All looks right by the app code!

@Lash-L Lash-L merged commit 4dbe17e into Python-roborock:main Jun 20, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

B01 /jobs writes (schedules + one-time room cleans) 401 — Hawk auth signs path only; POST/PUT/DELETE must sign the request body

2 participants