Is Primus safe?
Primus is medium risk. Starting a Primus attestation makes the extension register request listeners on the tab, record matched headers, cookies, bodies, and place them into proof params sent to the offscreen engine. An earlier test saw only padolabs.org traffic.…
Who publishes itPrimus Labs - no other listings under this identity
Primus Labs - no other listings under this identity
What this publisher told the store about itself, and the other listings that told it the same thing.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Attestation flow copies headers and cookies
Starting a Primus attestation makes the extension register request listeners on the tab, record matched headers, cookies, bodies, and place them into proof params sent to the offscreen engine.
An earlier test saw only padolabs.org traffic.
What actually happens
You start a Primus attestation for a data-source site.
The extension opens or uses a tab for that site as part of the proof flow.
The extension records matched requests from that tab and prepares them as proof inputs.
Headers, cookies, and POST bodies are copied when the active template says the request must be captured.
Fields the attestation path can copy from matched requests
- Session cookiesCookie: luma.auth-session-key=sess_7f4b3d2a91c845bb; csrf=bd6d2f3c1a994d1e (illustrative)
This can identify your signed-in session for the site used in the attestation.
- Authorization headerAuthorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMDAxIn0.Ji6Q0l3YbQ4L9wVmzK3qYsB5cR7n2qH8pW1dAeT0sYg (illustrative)
This can carry a bearer token or similar value that proves your account is signed in.
- Request bodyPOST body: {"accountType":"SPOT","timestamp":1720771200000} (illustrative)
This can include the submitted parameters for the account or API request being proved.
- Matched request URLhttps://api.binance.com/api/v3/account?timestamp=1720771200000 (illustrative)
This shows which account or API endpoint the proof flow matched during your attestation.
- Anti-CSRF headercsrf-token: ajax:734920156421 (illustrative)
This can link the copied request to the signed-in browser session for that site.
The code that does this
The shipped service worker captures headers and bodies before building proof parameters
Request listeners registered for all URLs
background.bundle.jsa.onBeforeSendHeadersFn = d, a.onBeforeRequestFn = f, chrome.webRequest.onBeforeSendHeaders.addListener(d, { urls: ["<all_urls>"], types: ["xmlhttprequest", "main_frame"]}, ["requestHeaders", "extraHeaders"]), chrome.webRequest.onBeforeRequest.addListener(f, { urls: ["<all_urls>"], types: ["xmlhttprequest", "main_frame"]}, ["requestBody"])Matched request headers are stored
background.bundle.jsconsole.log("captured request", f, JSON.stringify(e));const a = { headers: s, method: r, url: f, requestId: n, templateRequestUrl: p, type: e.type};i && (a.queryString = i), c(n, a), await lc()Request bodies are stored when present
background.bundle.jsif (r?.raw?.[0]?.bytes) { const a = new Uint8Array(r.raw[0].bytes), t = (new TextDecoder).decode(a); c(n, { ...e, body: JSON.parse(t) })}r?.formData && c(n, { ...e, body: r.formData, isFormData: !0})Stored requests become algorithm parameters
background.bundle.jsObject.assign(p, { reqType: "web", host: S, schemaType: b, requests: m, responses: T, uiTemplate: x, templateId: u, calculations: s, PADOSERVERURL: X, padoExtensionVersion: e}), await ka(p, t), qe(p, t), Je(p, t), c.formatAlgorithmParams = p, console.log("formatAlgorithmParams", p, t)Parameters are sent to the offscreen proof engine
background.bundle.jsconst a = Object.assign({ isUserClick: "true" }, s.formatAlgorithmParams), c = await D(), t = { ...a, clientType: c?.clientType || "" };await _({ activeRequestAttestation: JSON.stringify(a)}), console.log("pageDecode-algorithmParams", a), chrome.runtime.sendMessage({ type: "algorithm", method: "getAttestation", params: t})The WASM bridge calls callAlgorithm
offscreen.js_call(e, t = {}, i) { const s = { method: e, version: i || CLIENT_VERSION, params: t }, n = JSON.stringify(s); return Module.cwrap("callAlgorithm", "string", ["string"])(n)}getAttestation(e) { const t = getEffectiveVersion(e?.clientType); if (console.log("getAttestation AlgorithmInited=", this.initialized), !this.initialized) return JSON.stringify({ content: null, retcode: "2", retdesc: "Algorithm not initialized" }); const i = this._call("getAttestation", e, t); return console.log("getAttestation typeof res", typeof i, "res", i), i}PADO hosts used by the proof workflow
- api.padolabs.org
Production API base used for identity, event reporting, logs, and runtime configuration.
- api2.padolabs.org
Production WebSocket host hardcoded for algorithm-proxy and algoproxy endpoints.
Captured request
Observed during dynamic analysis along with GET https://api.padolabs.org/public/pado/identity; no algoproxy request body was recorded in that run.
Primus replays signed-in exchange API requests
Starting a Primus proof flow lets the extension retain headers from the data-source tab and replay matching exchange API requests with credentials included.
The confirmed path covers private Binance-style endpoints; no body was recorded.
What actually happens
You start a Primus proof flow that uses a signed-in exchange account.
The start handler marks the flow as a user click and sends the proof parameters to the attestation worker.
The extension can replay a matching private exchange API request with retained headers and browser credentials included.
The replayed response is used to validate account data before the proof is generated.
Fields used in the replayed exchange request
- Exchange session cookiesCookie: Binance session cookies redacted
Keeps the replayed request tied to your signed-in exchange session.
- Captured request headersUser-Agent: Mozilla/5.0; Accept: application/json
Preserves the same browser request context that the exchange page used.
- Private account endpointhttps://www.binance.com/bapi/asset/v3/private/asset-service/asset/get-user-asset
Points the replay at an account API that returns data about your exchange profile or assets.
- Proof-flow contextactiveRequestAttestation with isUserClick true
Links the replayed exchange response to the proof request that is being generated.
Captured request
Code-level verification confirmed the replay path for private Binance account APIs; no request body was recorded in the available evidence.
- Cookie
- redacted active exchange session cookies
The code that does this
Replay helper keeps headers and browser credentials
Readable fetch option builder
background.bundle.jsfunction ge({ url: e, method: a, body: c, header: t, isFormData: d}) { return { url: e, method: a, body: c, header: t || {}, isFormData: d }}function ye({ url: e, method: a, body: c, header: t, isFormData: d}) { const f = { method: "GET", headers: { "Content-Type": "application/json" }, credentials: "include", ...{ method: a, body: c, headers: t } }; ["POST", "PUT", "PATCH"].includes(f.method.toUpperCase()) && f.body && (i(f.body) || (f.body = JSON.stringify(f.body))); const b = "application/x-www-form-urlencoded"; if (f.body && t["content-type"]?.includes(b) || t["Content-Type"]?.includes(b) && d) { let e = i(f.body) ? JSON.parse(f.body) : f.body; f.body = (r = e, Object.keys(r).map((e => encodeURIComponent(e) + "=" + encodeURIComponent(r[e]))).join("&")) } var r; return { finalOptions: f, url: e }}async function ve(e, a) { const c = await fetch(a, e); if (!c.ok) throw new Error(`HTTP error! status: ${c.status}`); const t = c.headers.get("Content-Type") || ""; let d; return d = t.includes("application/json") || t.includes("+json") ? await c.json() : t.includes("text/") ? await c.text() : t.includes("application/octet-stream") ? await c.blob() : await c.text(), { data: d, contentType: t, finalUrl: c.url }}Readable replay call using stored headers
background.bundle.jsconst a = await Se({ ...f[e], header: f[e].headers, url: I, body: E});a && (w = a.data, A = a.contentType, T = a.finalUrl)The code that does this
The extension stores matching request headers before replay
const { requests: d} = t, { url: f, requestHeaders: b, method: r, requestId: n} = e;let i = "";const s = b.reduce(((e, a) => (e[a.name] = a.value, e)), {});let p = "";if (d.some((e => { if (!1 === e.needCapture) return !1; if (e.queryParams?.[0]) { const a = f.split("?")[0]; if (e.url === a) { const a = x(f, e.queryParams); a && (i = a) } } const a = u({ requestUrl: f, requiredUrl: e.url, urlType: e.urlType, queryParams: e.queryParams }); return a && (p = e.url), a }))) { console.log("captured request", f, JSON.stringify(e)); const a = { headers: s, method: r, url: f, requestId: n, templateRequestUrl: p, type: e.type }; i && (a.queryString = i), c(n, a), await lc()}The code that does this
A user-started flow launches proof generation
if ("start" === n) { if (s.startHandled) return void r({ ok: !0 }); s.startHandled = !0, s.phase = xe.ATTESTING, Oc(), chrome.alarms.create(bc, { periodInMinutes: .4 }); const { userInfo: e } = await w(["userInfo"]); e || Kc().catch((e => { console.log("ensureExtensionUserIdentity non-blocking error", e) })); const a = Object.assign({ isUserClick: "true" }, s.formatAlgorithmParams), c = await D(), t = { ...a, clientType: c?.clientType || "" }; await _({ activeRequestAttestation: JSON.stringify(a) }), console.log("pageDecode-algorithmParams", a), chrome.runtime.sendMessage({ type: "algorithm", method: "getAttestation", params: t })}Exchange hosts named by the verified replay path
- www.binance.com
Private account and asset endpoints used as replay targets during exchange proof validation.
- www.okx.com
Private profile endpoint named in verification evidence for the same authenticated replay pattern.