Wallets
The Built-In Wallet

The Built-In Wallet

Superhero can create and hold an æternity account inside the app itself, with no extension and no second app. The seed lives encrypted in your browser's own storage on that device, and every signature re-verifies you before the seed is decrypted.

It is a different thing from Superhero Wallet, the standalone extension and mobile app, though both hold the same kind of ak_... account.

Where it is offered

SurfaceCreate a walletImport an existing seedIn-page signing
Installed PWA (Add to Home Screen)YesYesYes
Plain browser tabYesNo, use Connect insteadNot installed as the signer

Creating a wallet (passkey or a fresh recovery phrase) is offered on every surface. Pasting a seed phrase or private key is offered only in the installed app: in a browser tab, "I already have a wallet" means Connect, which hands signing to the extension or the wallet app and never asks for your secret.

The in-page signer is installed only when the app is running installed and the active address belongs to this device's wallet. An externally connected wallet is never intercepted.

How the key is stored

mnemonic (BIP39, 12 or 24 words)
   │  encrypted (AES-256-GCM)
   ▼
sealed under a random data-encryption key (DEK)
   │
   ▼
one wrapped copy of the DEK per enrolled factor  ── passphrase · recovery code · passkey

One encrypted vault record per device. Every secret inside it is ciphertext; nothing is sent to a Superhero server.

Factors form an OR-set: any single enrolled factor unwraps the same DEK and opens the whole vault. That is what makes "passkey or passphrase" work, and it is also the security ceiling: the vault is only as strong as its weakest enrolled factor. A weak passphrase is not made safe by also having a passkey. The last factor can never be removed, because a vault with no factors is permanently unopenable.

The unlock factors

FactorHow the key is derivedWhere the secret lives
PassphraseArgon2id (memory-hard, so an offline attack on a copied vault is expensive)Your head
Recovery codeHKDF from 128 generated bitsWherever you wrote it
PasskeyHKDF from the WebAuthn PRF outputYour platform authenticator

Passphrase

The passphrase is scored locally with zxcvbn, with no network call. The rules:

  • Minimum zxcvbn score 3 of 4 ("safely unguessable").
  • A numeric-only string is rejected as a PIN.
  • The generator offers five words from the BIP39 English list, about 55 bits.
  • If the estimator fails to load, Create stays disabled and you are offered a retry; there is no fallback to a length rule.

The passphrase cannot be changed after creation. To change it, recreate the wallet from the recovery phrase. See Backup & Recovery.

Recovery code

A 128-bit code shown once at enrollment, rendered as eight dash-separated groups of four uppercase hex characters. Parsing tolerates case, spaces and dashes.

The recovery code is passkey-independent, not device-independent. It unwraps the DEK inside this device's vault record. If that record is gone, the code opens nothing.

Passkey

The passkey factor uses the WebAuthn PRF extension, which yields a stable 32-byte secret for a given credential. The ceremony requires user verification and a resident (discoverable) credential.

Some security keys report PRF support but return no result; those are treated as unsupported. If the first ceremony returns no PRF output, the app runs a follow-up assertion to obtain it, which is why you may see two prompts.

The Relying Party ID is pinned at build time from VITE_WEBAUTHN_RP_ID, defaults to superhero.com, and is never derived from the page's own host. A bundle served from another host cannot use your Superhero passkey. When that value does not match the origin, the passkey option fails with no OS prompt at all. See Troubleshooting.

Two ways a wallet is created

From a recovery phrase

A 12-word BIP39 phrase is generated, shown, and verified by re-entry before any vault exists. Then a passphrase factor creates the vault. The recovery code is enrolled next and is mandatory. A device passkey is offered after that and is optional.

Import follows the same path minus the show/verify screens. The phrase is normalized (trimmed, whitespace collapsed, lowercased) and checked against the BIP39 wordlist and checksum before a vault is created, so a typo cannot seal a wrong seed.

From a passkey

One ceremony does everything. The passkey is enrolled, and the BIP39 mnemonic is derived from its PRF output at a fixed, public salt, so there is no phrase screen. The seed derivation and the key that wraps the DEK use separate derivations, so neither can be computed from the other.

The salt is fixed rather than random so that the seed can be re-derived on any device the passkey reaches, even after the vault is gone. That makes the passkey a real backup only if it syncs (iCloud Keychain, Google Password Manager). A device-bound platform credential such as Windows Hello dies with its device, and there is no phrase reveal or export screen in the app, so for those users the passkey is the only copy.

Restoring on a new device runs a discoverable assertion, derives the wallet the credential implies, and shows you the derived address before persisting anything. The confirm screen also checks whether that address has on-chain activity: a pristine account is a warning that you picked the wrong passkey, because a phrase-created wallet's device passkey evaluated at the seed salt derives a valid but empty different wallet.

Key derivation

PurposePathCurve
æternity spending keym/44'/457'/<index>'/0'/0'ed25519, SLIP-0010
Nostr chat identitym/44'/1237'/0'/0/<index>secp256k1

The æternity path uses the SDK's own account factory, so an account created here is byte-identical to importing the same phrase into the Superhero Wallet extension or the native app at the same index. There is nothing proprietary to be stranded in.

Signing

Every signature runs the same sequence, with no fast path:

  1. Run the unlock (a WebAuthn ceremony or the passphrase prompt). This happens on every signature.
  2. Unwrap the DEK, unseal the mnemonic, derive the account key. All transient.
  3. Check that the derived key produces the advertised address. If not, refuse to sign.
  4. Sign, then drop every reference.

There is no cached unlock and no session-cached seed. The vault record is reloaded from storage on every signature, so adding a factor takes effect immediately. If no confirmation prompt is mounted, a signing request is rejected immediately rather than left hanging, and only one prompt renders at a time.

The chat identity is the one cached key. The Nostr chat key is derived through the same unlock path and then cached in memory for a rolling 30-minute idle window; chat activity resets the timer, and the key is dropped when it expires or on an explicit lock. The unlock prompt says so. Nothing else derived from the seed is cached. The unlock returns Nostr keys only, never the mnemonic, the DEK or the æternity spending key.

What the confirmation shows

The transaction is decoded into named rows, with two deliberate behaviours:

  • Decode, never re-encode. The bytes signed are always the original string. A decoding bug can mislead; it cannot alter what gets signed.
  • Fail closed. If the envelope, a contract call's arguments, or a wrapped inner transaction cannot be decoded, the prompt refuses to offer approval at all. There is no "show raw bytes and approve anyway".

Recognised calls are named in plain language: publish a post, buy tokens, sell tokens, create a community, transfer, the four AEX-9 allowance operations, add and remove liquidity, wrap and unwrap AE, add a poll, cast or revoke a vote, delegate and revoke delegation, and the invitation-code operations. A call whose argument shape does not match is shown as an unrecognised contract call with every argument rendered raw.

Two rows to read every time:

  • "Gas, at most": gas is charged on top of the network fee, so a prompt showing only the fee understates what a contract call can cost.
  • "Name price": on a name claim this is the largest amount by far.

Token amounts are shown in raw base units, not scaled by decimals: decimals are a per-token on-chain fact the decoder does not look up, and inventing a scale would misstate what you are approving.

Where the key material lives

ThingWhereEncrypted?
The vault record (sealed mnemonic, wrapped DEKs)IndexedDB, database superhero-wallet, store vault, key vaultYes
Wallet manifest (addresses, derivation indexes, labels, active address)localStorage, key wallet.inlineManifestNo, public data only
Mnemonic, DEK, derived keysMemory, inside one unlock-to-sign scopen/a
Anything at allA Superhero serverNothing is sent

What clearing site data destroys

Clearing site data (or "Cookies and site data") for superhero.com deletes the IndexedDB vault. Once the vault record is gone:

  • Your passphrase opens nothing, because there is no wrapped DEK left to unwrap.
  • Your recovery code opens nothing, for the same reason.
  • Your passkey still exists in your authenticator, but for a phrase-created wallet it only wrapped a DEK that is now gone, so it opens nothing either.

The same loss happens without you touching anything: IndexedDB is evictable, and Safari's Intelligent Tracking Prevention can evict it after roughly seven days without interaction. A successful save is never a durable backup.

What survives a wiped vault:

You haveSurvives?Why
The written 12/24-word phraseYesStandard BIP39 on the standard path, so it re-imports anywhere.
A syncing passkey on a passkey-created walletYesThe seed re-derives from the PRF output at the fixed public salt.
A device-bound passkey (e.g. Windows Hello) on a passkey-created walletOnly on that deviceIt cannot leave the device, and there is no export screen.
A passkey on a phrase-created walletNoIt wraps a DEK; it does not derive the seed.
The recovery code aloneNoIt unwraps a DEK; it does not derive the seed.
The passphrase aloneNoSame.

Losing only localStorage is survivable: unlock once and the app rebuilds the manifest by re-deriving addresses from index 0 upward.

Erasing the wallet deliberately is available from the "this device already has a wallet" screen, and takes two taps because for a passkey wallet it is unrecoverable.

Related