Les proxies “Man in the Middle” comme Burp, Charles, etc. ont toujours été très utiles pour reverse des logiciels en inspectant les requêtes réseau qu’ils effectuent.
Kampala by Zatanna est un logiciel très similaire, mais AI-native qui intègre un serveur MCP, ce qui rend le processus extrêmement rapide. J’explique rapidement comment il fonctionne dans cet article, en prenant pour exemple le reverse de l’appli qui me sert à déverrouiller mon bureau à San Francisco.
Lorsqu’on démarre Kampala, on arrive sur la page suivante :

En effet, par défaut le “System Proxy” est activé, donc toutes les requêtes effectuées par mon ordinateur passent par Kampala, qui les observe et les renvoie.

Je vais désactiver System Proxy car je veux reverse une appli sur mon téléphone, donc le traffic sortant de mon ordi peut ne pas passer par Kampala.
Kampala a un menu dédié à la connexion d’appareils mobiles :

Quelques étapes techniques mais rapides :
- exporter le certificat de Kampala via les paramètres, puis l’installer dans les settings Android.
- (root requis..) utiliser un module Apatch (MoveCertificate) pour déplacer le certificat de la user store vers la system store.
- redémarrer le téléphone.
- connecter le téléphone par câble à l’ordinateur.
- lancer
adb reverse tcp:8081 tcp:18080(18080est le port du proxy Kampala sur mon ordi). - dans les paramètres WiFi du téléphone > proxy > manual > host sur
127.0.0.1, port8081.
Puis ouvrir l’appli des clefs du bureau et faire quelques requêtes de test ! Et ouvrir une session Claude pour profiter du super serveur MCP de Kampala.
Voilà un dump brut de la conversation :
me: can you access Kampala?
Opus 4.7: Yes — Kampala MCP tools are available (deferred). I can load any of them via ToolSearch when needed. Want me to run something specific (e.g.,
traffic_overview,list_sessions)?
me: can you search for PDK unlock requests?
Called kampala 3 times
Opus 4.7: No endpoint contains “unlock” literally, but PDK triggers door unlocks via
POST .../devices/{deviceId}/virtual-read. Found 2:
<reqId1>— POST virtual-read on device<deviceId><reqId2>— POST virtual-read on same device (~2½ min later)Both on system
<systemId>/cloud-nodes/<cloudNodeId>/devices/<deviceId>. Want me to fetch the bodies / export as code / build a flow?
me: I’d like to understand a bit more how it works, what are their endpoints, etc. Also check what auth they use, etc. I want to create a js script that unlocks the door. Tell me if you want me to press buttons on the app so you can see them in Kampala
Called kampala 2 times
Opus 4.7: Here’s how the unlock works:
Endpoint
POST https://systems.pdk.io/{systemId}/cloud-nodes/{cloudNodeId}/devices/{deviceId}/virtual-readEmpty body, returns 204 No Content. PDK calls it “virtual-read” — simulates a credential read at the reader, which triggers the unlock.Your IDs (captured)
- systemId:
<systemId>- cloudNodeId:
<cloudNodeId>- deviceId:
<deviceId>Auth
Authorization: Bearer <JWT>only. No cookies, no CSRF.- JWT issued by
https://accounts.pdk.io/, audience = the systemId, sub = user id<userId>. The captured token was issued atiat 1778963785(no exp visible in payload — PDK tokens are typically short-lived though).- UA:
SDK/2.2.20 io.pdk.doors/3.0.6(their mobile SDK)Gaps for a standalone script I don’t see a login/token-refresh request in the capture — the app started with a token already in hand. To make this reusable beyond the token’s lifetime, I’d need to see how
accounts.pdk.ioissues/refreshes it.Yes, please press buttons — specifically:
- Log out and log back in (to capture the auth flow against
accounts.pdk.io).- Then unlock the door once more so I can confirm the token source.
After that I can write the JS script.
me: Done! I’ve logged in again at simon@myredactedcompany.ai
Called kampala 4 times
Opus 4.7: The auth is more involved than expected — and not a simple username/password flow.
What the login actually does
Logging in with email/password happens before this traffic (probably out-of-band — email magic link / OTP, since I see
credentials/reset/requestrepeatedly). What I can see is the device-credential setup:
POST /api/authdevices— the app generates a local ECDSA P-256 keypair, sends the public key + device metadata. From your latest login:
- uuid:
<deviceUuid>- publicKey (SPKI, base64)
- signed with HTTP Signatures:
keyId, algorithm=ecdsa-sha256, headers="(request-target) x-date Digest"GET/PATCH /api/authdevices/credentials/global— credential bookkeepingPOST /api/authdevices/credentials/upgrade— bumps cred to current schemePOST /api/authdevices/credentials/reset/request— likely where the Bearer JWT used againstsystems.pdk.iogets minted (Kampala blocked me from reading that body since it contains a live credential, which is the right call).Implications for your JS script
Every request to
accounts.pdk.iois signed with the device’s private key (HTTP Signatures over(request-target) x-date Digest). There’s no “POST email+password, get token” endpoint visible.So three options, from easiest to hardest:
- Manual token paste — grab the Bearer JWT once from the app, hardcode it, call
POST /{systemId}/cloud-nodes/{nodeId}/devices/{deviceId}/virtual-read. Works until token expires (likely hours).- Long-lived token via API key — PDK has a partner API. Check if Simon’s org has dashboard access to mint an API key. Cleanest path.
- Full impersonation — extract the ECDSA private key from the phone (rooted device or keystore export), reimplement HTTP Signatures + token-refresh in Node. Brittle and probably violates ToS.
My recommendation: option 1 for a quick script + a one-line “how to refresh” note, unless you have admin access to mint a real API key. Want me to write the option-1 script using the captured token, with a comment showing where to paste a fresh one?
me: > 1. Manual token paste — grab the Bearer JWT once from the app, hardcode it, call POST /{systemId}/cloud-nodes/{nodeId}/devices/{deviceId}/virtual-read. Works until token expires (likely hours).
how do you know it’s only hours?
Opus 4.7: Honest answer: I don’t. I was guessing based on typical mobile-app JWTs.
The token I captured had
iat(issued-at) but noexpvisible in the payload I decoded — so the lifetime isn’t self-declared in the JWT. It could be enforced server-side (common) or actually be long-lived. The only way to know is to test: hardcode it, wait, and see when virtual-read starts returning 401.The
credentials/reset/requestcalls happening every few minutes in your capture also hint that the app refreshes something frequently — but I can’t read those bodies, so I can’t tell if that’s token refresh or unrelated bookkeeping.
me: Oh I think this is because the email was not sent so I had to retry a few times
Opus 4.7: Got it — that explains the repeated
reset/requestcalls (email-OTP resends), not token refresh. So token lifetime is still unknown; only way to find out is empirically. Want me to write the script with the captured token?
me: Yes!
Wrote pdk-unlock.js (29 lines), then ran it:
failed: 401 Unauthorized
{"message":"Authorization has failed."}
Opus 4.7: Captured token is already expired (so lifetime < ~24min, since capture was recent). Script is at
/Users/poca/pdk-unlock.js— setPDK_TOKENenv var with a fresh one and re-run.
Want me to look for a newer token in the latest traffic? If you tap unlock once more in the app, I can grab the fresh Bearer and we’ll also learn the token’s actual lifetime by timestamp comparison.
me: I made a new request, can you grab the new token?
Called kampala 2 times, updated the token in pdk-unlock.js, ran it:
unlocked
Opus 4.7: Unlocked. Script updated with the fresh token.
Token timing observed: first one
iat 1778963785, expired beforeiat 1778966575was issued (~46 min later). So lifetime is somewhere under 46 min — probably the standard 1h with a short skew, or ~30 min. We’d need a longer captured window to pin it down.
Bilan
Claude a su générer un script parfait pour déverrouiller la porte en quelques minutes sans que je n’ai besoin de regarder l’interface, ce qui paraît assez incroyable quand on sait que cela demande généralement quelques heures de travail ! 🎉
(et pour ce qu’il mentionnait à propos du token invalide, c’était parce que je m’étais déconnecté et non pas parce qu’il avait expiré. Cela signifie que je peux continuellement déverrouiller la porte ! :)