Is Backpack safe?
Backpack is medium risk. Backpack's wallet creation makes a WebAuthn passkey, calls 2231dcc2.sslip.io/v0/derive-public-key, and builds wallet init records from the returned keys. The POST carries an embedded Bearer JWT. Testing couldn't reach this gated branch.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Passkey Wallet Setup Posts Derived Public Keys
Backpack's wallet creation makes a WebAuthn passkey, calls 2231dcc2.sslip.io/v0/derive-public-key, and builds wallet init records from the returned keys.
The POST carries an embedded Bearer JWT.
Testing couldn't reach this gated branch.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You start Backpack's passkey wallet creation flow.
The branch is tied to browser WebAuthn passkey creation, not the standard mnemonic wallet flow.
The extension derives public keys and prepares wallet records from the passkey result.
The service worker posts derivation inputs to 2231dcc2.sslip.io with an embedded Bearer token.
The WebAuthn-gated passkey branch was not executed during testing, so no request body or response body was recorded.
- Content-Type
- application/json
- Authorization
- Bearer <redacted embedded JWT>
- Derived public key9xQeWvG816bUx9EPfN8uR5WzQn3s9Y7vZyK9G6aYb7H8
Lets the new passkey wallet link to a public wallet address per chain. Illustrative; no live request body was recorded.
- Derivation domained25519
Shows whether the requested derivation is for an Ed25519 or Secp256k1 wallet family.
- Passkey credential IDV2ViQXV0aG4tY3JlZGVudGlhbC1pZC0yMDI2
Ties the wallet init record to the browser passkey created during setup. Illustrative; no live request body was recorded.
- Embedded JWT claims{"v":1,"key_type":"distribution_channel","partner_id":"backpack","iat":1773125535,"exp":1804661535}
Identifies the request as Backpack distribution-channel traffic and gives the token's validity window.
The request helper posts JSON to the configured base URL
class OneClickClient { constructor(options) { const baseUrl = options.baseUrl.trim(); this.baseUrl = baseUrl.startsWith("http://") || baseUrl.startsWith("https://") ? baseUrl : `https://${baseUrl}`; this.authToken = options.authToken; } async derivePublicKey(predecessor, path, domainId) { const response = await this.post("/v0/derive-public-key", { path, domainId, predecessor }); if (!response.ok) { const text = await response.text().catch(() => ""); throw new Error(`OneClickClient.derivePublicKey failed: HTTP ${response.status} — ${text}`); } const publicKey = await response.text(); const prefix = `${domainId}:`; if (!publicKey.startsWith(prefix)) { throw new Error(`OneClickClient.derivePublicKey: unexpected response format — expected "${prefix}...", got "${publicKey.slice(0,64)}"`); } return publicKey; } async post(path, body) { const headers = { "Content-Type": "application/json" }; if (this.authToken) headers.Authorization = `Bearer ${this.authToken}`; return fetch(`${this.baseUrl}${path}`, { method: "POST", headers, body: JSON.stringify(body) }); }}The passkey branch configures sslip.io and builds wallet initialization data
async function createPasskeyWalletInits(networkIds, options = {}) { const supportedNetworks = networkIds.flatMap((networkId) => { const config = q6[networkId]; return config ? [{ networkId, config }] : []; }); if (supportedNetworks.length === 0) { throw new Error("No supported networks selected for passkey wallet"); } const passkeyWallet = await c9.createWallet({ displayName: options.displayName }); const { address, credentialId } = passkeyWallet; const needsEd25519 = supportedNetworks.some((entry) => entry.config.domain === z6.Ed25519); const needsSecp256k1 = supportedNetworks.some((entry) => entry.config.domain === z6.Secp256k1); const client = new j6({ baseUrl: "https://2231dcc2.sslip.io", authToken: "<redacted embedded JWT>" }); const [ed25519PublicKey, secp256k1PublicKey] = await Promise.all([ needsEd25519 ? client.derivePublicKey(address, "", z6.Ed25519) : Promise.resolve(""), needsSecp256k1 ? client.derivePublicKey(address, "", z6.Secp256k1) : Promise.resolve("") ]); return { walletInits: supportedNetworks.map(({ networkId, config }) => { const derivedPublicKey = config.domain === z6.Ed25519 ? ed25519PublicKey : secp256k1PublicKey; return { type: By.PASSKEY, blockchain: wy(networkId), blockchainId: networkId, walletType: config.walletType, publicKeyInfo: { publicKey: config.deriveAddress(derivedPublicKey) }, credentialId }; }) };}The passkey setup is gated by browser WebAuthn creation
async createPasskey(options) { if (!navigator.credentials) throw new Error("WebAuthn not supported in this browser"); const challenge = options.challenge ?? crypto.getRandomValues(new Uint8Array(32)); const userId = crypto.getRandomValues(new Uint8Array(32)); const credential = await navigator.credentials.create({ publicKey: { challenge: challenge.buffer, rp: { id: e9, name: "Backpack" }, user: { id: userId.buffer, name: options.displayName, displayName: options.displayName }, pubKeyCredParams: [ { alg: -7, type: "public-key" }, { alg: -8, type: "public-key" } ], authenticatorSelection: { authenticatorAttachment: "platform", userVerification: "required", residentKey: "preferred" }, timeout: 60000, extensions: { prf: { eval: { first: X6.buffer } } } } }); if (!credential) throw new Error("Failed to create passkey"); const extensionResults = credential.getClientExtensionResults(); const prfEnabled = extensionResults.prf?.enabled ?? false; if (!prfEnabled) { throw new Error("PRF extension not supported on this device. Requires iOS 18+, Chrome 108+, or Edge 108+."); } const prfOutput = extensionResults.prf?.results?.first; return { id: credential.id, publicKey: new Uint8Array(credential.response.getPublicKey() ?? []), prfEnabled, prfOutput: prfOutput ? new Uint8Array(prfOutput) : undefined };}- 2231dcc2.sslip.io
Receives passkey public-key derivation POSTs from the service worker during passkey wallet setup.