Skip to content

Add V2 region public-key recovery from SHA-384 signatures - #84

Merged
Lash-L merged 1 commit into
mainfrom
research/v2-region-key-recovery
Sep 13, 2026
Merged

Lash-L merged 1 commit into
mainfrom
research/v2-region-key-recovery

Conversation

@Lash-L

@Lash-L Lash-L commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@Lash-L

Lash-L commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Hello! This is a branch I'm going to use to attempt to get V2 working.

I was able to get this (seemingly) working from a board I sent to @dgiese who was able to do a firmware dump for me with his equipment. My board is still with Dennis so I cannot do the real hardware test myself. It may not work on the first go. Rather than waiting, I'm hoping one of you is able to and we can iterate from there. I'm pinging everyone who has offered to help. (Sorry for the mass ping)

@Flash1232 @fhaala @Kombustor @ppluciennik @181207 @pieterbos @calebsw83 @manukurusu @IIgorrrrr

You can point an agent at this or do it yourself :)

Testing steps for this PR (Docker Compose)

This is an experimental test of the V2 GET /region format . First verify public-key recovery, then check whether the vacuum advances to NC registration and MQTT. Full V2 onboarding has not yet been demonstrated on hardware.

  1. Check out the branch on the server machine.

    For a fresh checkout:

    git clone --branch research/v2-region-key-recovery https://github.com/Python-roborock/local_roborock_server.git
    cd local_roborock_server

    For an existing installation, back up config.toml, data/, and secrets/, then run:

    git fetch origin
    git switch research/v2-region-key-recovery
    git pull --ff-only

    The locally tested commit is 22943d8. Build from this checkout so the running server includes the PR changes.

  2. Configure a fresh installation, or retain the existing configuration.

    For a fresh installation:

    uv sync
    uv run roborock-local-server configure

    Follow the installation guide for DNS and certificates. The api-... hostname must resolve to the server's LAN address. Default ports are HTTPS 555 and MQTT TLS 8881; use the configured ports if they differ.

  3. Build and start the server.

    docker compose up -d --build
    docker compose logs -f roborock-local-server

    Open https://YOUR_API_HOST:555/admin, complete cloud import so the vacuum appears, and enable Allow new app logins, onboarding, and first-time vacuum connections. If custom ports are used, keep the Compose port variables aligned with the configuration as described in the installation guide.

  4. Run terminal onboarding from a second computer with Wi-Fi.

    Check out the same branch on that computer, then run:

    uv run start_onboarding.py --server YOUR_API_HOST:555

    Enter the admin password and Wi-Fi details and select the vacuum. Follow the prompts to perform the model's Wi-Fi reset, join its hotspot, send the onboarding packet, and reconnect the computer to normal Wi-Fi. Use the model-specific Wi-Fi reset procedure; a full factory reset is not needed.

  5. Collect samples and wait for public-key recovery.

    The CLI and dashboard still report Unsupported for V2 in this PR. That status can end CLI polling early while server-side recovery continues. The terminal tool still offers retry for another pairing cycle.

    Leave the server running and allow several minutes for recovery. Check Public Key determined in the admin dashboard, or public_key=True in the CLI status. If the key is still missing, use retry and repeat the pairing cycle. Keep the saved server data between attempts.

    The displayed query sample count can remain zero, because V2 uses header signatures. Recovery requires at least two distinct valid requests; the verified offline capture recovered successfully from three. If several cycles produce no key, collect the results below before doing more retries.

  6. After the key is recovered, run one more pairing cycle.

    Allow approximately five minutes and record the furthest stage reached:

    Observation What it establishes
    Public key recovered V2 key recovery worked
    NC registration appears The vacuum progressed beyond /region
    Authenticated MQTT traffic and status updates Local connectivity is working
    A status request receives a response through the local server The command path works
    The vacuum reconnects after a normal reboot The connection survives a restart

    An HTTP 200 response or a recovered key alone does not prove complete onboarding. The unsupported banner can remain even if later stages work.

  7. Report the result.

    Include the model identifier, firmware version, configured region, Git commit, number of pairing attempts, whether the public key recovered, and the furthest stage reached. Include any TLS or MQTT authentication errors and approximate attempt times.

    Keep these files locally (default Compose paths):

    data/state/device_key_state.json
    data/runtime/decompiled_http.jsonl
    data/runtime/decompiled_mqtt.jsonl
    

    Share redacted excerpts initially. These files can contain credentials and private device information. Preserve the original captures locally so the exact protocol bytes remain available for investigation.

@Flash1232

Copy link
Copy Markdown

Just from a quick initial test:

I've updated my stack (deployed on HA, custom fork repo with this branch) and ran the onboarding again (with your exact branch). So far it again seems to only reach /region as was the case before. Multiple rounds, no query samples. Samples don't increase, pubkey not restored. Do I have to clean some state as I can still see 4 header_samples (presumably from the last attempts)? In that case, I will just clean the decompiled jsonc file. Or am I misunderstanding what to look out for?

Glad to assist with anything if there's need.

@Lash-L

Lash-L commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for testing it out @Flash1232

First thing I would check is that the HA is using the right image/tag. The add-on config points towards an image and I worry it is still using the old code.

After a attempt, does a new entry appear in header_samples in device_key_state.json? Can you send the version, method, path, signature_len, query, nonce, and ts?

As well share the recovery dict in that same file?

@pieterbos

pieterbos commented Sep 13, 2026

Copy link
Copy Markdown

Could you maybe publish the home assistant build for 22943d8 (or later if you updated), and update the tag in the config for the home assistant app/add-on in your branch? It should the be possible to add the custom repository in home assisant, by using https://github.com/Python-roborock/local_roborock_server#<branch_name> , as documented in https://developers.home-assistant.io/docs/apps/presentation/#offering-stable-and-canary-version

I am happy to test, but I need a second computer, and home assistant is the easiest way to accomplish that at the moment for me. Otherwise I have to setup a second Arm single board computer - I have one lying around somewhere, but it'll take longer before I can make time to set that up.

@Lash-L
Lash-L merged commit a917b5e into main Sep 13, 2026
3 checks passed
@Lash-L

Lash-L commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Yes! @pieterbos good idea. I merged this PR and will do the rest later tonight

@Flash1232

Flash1232 commented Sep 13, 2026

Copy link
Copy Markdown

Should be the correct version. The device_key_state.json file only ever updates the recovery section to
"state": "collecting", "note": "Need at least 2 query signature samples (0 captured)."

Oddly enough, "header_samples" stays empty/ as-is even though I can clearly see v2 /region requests (same as those I shared initially in the Saros 20 Sonic ticket) in the decompiled jsonc file. I tried manually emptying the "header_samples" in the device key state json but it made no difference.

But as you've figured out the correct format, I was able to manually get the public key from those decoded payloads which I could manually insert it into the json. Unfortunately, even though the onboarding web view now says "ready" and I ran another cycle, the robot didn't connect (no NC_Prepare).

Maybe I should try a completely blank slate and re-import from cloud first again. Will try this in the coming days if nobody else did already.

Thank you again for all your efforts!

@Lash-L

Lash-L commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Hmm maybe i'm missing some of the routing logic for v2/ I still have some of my placeholder logic that discards v2 data... I'll take a deep looker.

For now, I have published a beta version - will be helpful going forward. it is "Roborock Local Server Beta" you may have to refresh your apps page @pieterbos

@Flash1232 Trying to think through this - can you check that modulus_hex is under the right DID, did you try restarting the addon. What does the region look like in decompiled_http.jsonl?

@pieterbos

Copy link
Copy Markdown

It recovered the public key with 2 header samples. Took 335 seconds, as this was running on an Odroid N2 (+?) Arm board, on a single core.

This is a QRevo Edge 2 Set, "model": "roborock.vacuum.a298",. Will report firmware version later - it's the most recent one.

The second pairing cycle however does not finish. It does record a POST to /nc. I see no working MQTT Connection. Do you need any more information? I'll leave the server running, but will reconnect my vacuum to the normal app for now :)

"onboarding": {
      "required_steps": [
        "region",
        "nc_prepare"
      ],
      "step_labels": {
        "region": "Region",
        "nc_prepare": "NC Prepare",
        "login_key_sign": "Key Sign"
      },
      "missing_steps": [
        "nc_prepare"
      ],
      "has_required_messages": false,
      "has_public_key": true,
      "public_key_ready": false,
      "status": "unsupported",
      "guidance": "This vacuum uses the v2 /region onboarding flow, which is not supported by local_roborock_server yet. These models usually stop after /region and never reach NC Prepare.",
      "unsupported": true,
      "unsupported_reason": "region_v2",
      "key_state": {
        "query_samples": 0,
        "header_samples": 2,
        "max_signature_len": 512,
        "has_modulus": true,
        "recovery_state": "recovered",
        "recovery_note": "Recovered 4096-bit public key in 335.7s.",
        "recovery_error": "",
        "recovery_started_at": "2026-09-14T21:26:14.629923+00:00",
        "recovery_finished_at": "2026-09-14T21:31:50.351501+00:00"
      }

(this log is in Local time, so 2 hours ahead of other timestamps)

2026-09-14 23:41:50,063 [INFO] [real_stack.api] GET /region host=api-rr.<REDACTED>:555 route=region body_sha256=e3b0c44298fc1c14
2026-09-14 23:41:50,429 [INFO] [real_stack.api] POST /nc host=api-rr.<REDACTED>:555 route=nc_prepare body_sha256=1121c3b1ad8dbfa3

@pieterbos

Copy link
Copy Markdown

Firmwre v02.15.44. Let's encrypt certificate. I can try another one, but I assume this is not the problem as it completed some succesful requests.

@Lash-L

Lash-L commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

@pieterbos TY. I was being overly optimistic on the changes I needed to make, but this is good progress. We are building the public key which is great.

I published a new pre release version, give it a try? I believe the issue is that the request uses multipart on your device.

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.

3 participants