Stores nothing reads any more kept whatever they held when they were
copied: the pre-rename addons-data/ folder, the bchwallet.json left by
the Aegis absorb, and parse-failure copies. For Aegis before 0.31 that
could include the master password behind only an unsealed 6-digit PIN
and the stay-unlocked blob. Those two keys are removed from such copies
at startup; nothing else in them is touched, and the live store is not
among them. extensions-backups/, which kept a copy of an add-on on
every reseed and update forever, is pruned to the newest three per
add-on.
Reserved ids came from the current bundle only, so an add-on dropped
from a later release became an id anyone could publish under, and the
newcomer inherited its extensions-data store and vault.derive
namespace. Every id that has shipped is now reserved permanently.
A community install strips category and absorbs and asks the user, but
an update of the same extension was staged and promoted verbatim, so
version 2 could claim first-party placement or quietly add
capabilities and page-inject origins. Publisher-signed updates now get
the same manifest rewrite, and one that asks for new capabilities or
new pages is not staged; the user approves it by reinstalling from
theseus.x.
The right-hand panel had neither a will-navigate nor a window-open
handler, so a link in a panel navigated the privileged view itself to
a remote page that kept sidebar-preload, and window.open made a bare
window with it; panel events were routed by the selected panel id, so
Aegis's state (every address, balances, WizardConnect sessions) would
then have reached that page. Both panels now open web links as tabs and
refuse to navigate away from file://, and events go only to a view that
has the add-on's own page loaded.
The approval box had no height limit inside a fixed mask, so a long
message or many rows pushed the end of the text and the buttons out of
view. The box now scrolls, long values scroll in place, and the
buttons stay pinned at the bottom.
wallet-imports-signer hands out raw seeds and WIFs while the vault is
open, and hermes-* sends and reads the user's Nostr messages; none of
these handlers checked who was asking. No preload exposes the
wallet-imports channels (add-ons use the vaultImports shim), so they
are now Settings-only like the password channels; the Hermes channels
answer only the Messages window.
A dapp could request a signature and then call alert(): the page's
sheet was raised over the approval, and closing it focused the page
again while the approval's buttons were already armed, so a
double-click on the sheet's OK landed on Approve. Page dialogs and
Theseus's own sheets are now held until the approval or unlock prompt
is answered, and nothing hands focus to the page while one is open.
bcnr.installExtension is in every page's main world, and install links
were honoured from any page and any frame, with no user gesture. Any
site could raise the install sheet timed so that a double-click landed
on Install, whose two buttons are always in the same place; an
installed community extension runs in the main process at once. The
call and the links now work only from theseus.x's top frame, the call
needs a real click (checked in the isolated world), and Theseus's own
sheets ignore every choice except Cancel for 800 ms, like the approval
overlay.
On a host with api.vault.pin, Aegis no longer keeps a PIN of its own:
its lock-screen, reveal and transaction pads send the digits to the
Theseus vault PIN, which is checked in main against the one strike
counter Settings, the unlock prompt and Pithos also use. The vault is
opened there, and the panel receives a single-use proof (two minutes)
where it used to receive the master password; vaultUnlock, revealSecret
and pinGateSatisfied accept it, still bound to the request it was
entered for. The master password no longer passes through Aegis or its
panel for a PIN entry.
An existing Aegis PIN moves to the vault PIN on its first correct entry.
If Theseus already has a different PIN, the user is asked once which
one to keep. Setting and removing the PIN act on the vault PIN, and
Settings says that it is shared. Hosts without the API (Theseus
0.3.74-0.3.76) keep Aegis's own PIN exactly as before.
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
will-navigate and the window-open handler routed every wiz:// link to
the wallet with the top-level URL's origin, whichever frame raised it.
An ad iframe on a trusted dapp could window.open() a pairing URI and
the pairing prompt would name the trusted site; one click on Pair gave
the attacker the wallet's xpubs and a standing signing channel. A
link must now come from the main frame (navigation initiator, or for
window.open the referrer's origin), and only on pages where the wallet
is allowed by the inject policy.
Privacy still said there is no persistent password manager and pointed to
Bitwarden or KeePass, and Passwords said autofill was "phase 2". Both
contradicted the encrypted vault and the address-bar fill button Theseus
has had for a while.
- Electrum: a reply larger than 8 MB ends the connection; a server
streaming without newlines used to grow the buffer without bound and
re-parse it on every chunk, on the Electron main thread.
- Overlay amounts are formatted from integer strings: 0.1 ETH read
0.100000000000000006, and 1 ETH + 1 wei read 1.
- The panel escapes a broadcast txid shown without an explorer link.
- A TPM refusal hands back only its own PIN attempt, not the count
before it, which could undo guesses made in parallel.
- BCH WIF imports must carry this network's version byte and a valid
compression flag; a 64-byte Solana keypair must have a public half
that matches its secret half.
- WizardConnect: the overlay shows the network fee (exact, as
SIGHASH_UTXOS commits to the inputs), marks outputs to this wallet's
own addresses, lists every token input and every output instead of
"... and N more" (more than 30 is refused), and a very high fee or
token inputs get the danger button.
- DAI permits: expiry 0 reads "never expires", and the allowed flag is
read the way the encoder signs it.
Dapp calls resolved their wallet afresh every time: Ethereum took the
first ready wallet on the site's chain, Solana and Tron the sidebar
selection. A site connected to wallet B was served wallet A, choosing
another wallet in the sidebar re-pointed every connected Solana/Tron
site and told it the new address, and a connected site could list
every EVM wallet's address by looping wallet_switchEthereumChain and
reading the account after each switch.
Grants now record the wallet id and address the user approved, and
calls use exactly that wallet (older grants are bound on first use; a
missing wallet is an error, not a substitute). A switch to a chain
whose wallet has a different address is asked for. A site can no
longer "add" a built-in chain id with its own RPC and get a second
mainnet wallet; chainMeta has no chainId for built-in networks, so
that check never matched.
The HD wallet's plan listed the change output as a recipient with no
address, so a dapp payment overlay showed "To #2: undefined" and an
Amount and Total inflated by the change; the imported wallet's total
counted change and left out the fee. Plans now list payees only, with
total = payees + fee.
A site's memo went on-chain as OP_RETURN data without appearing on the
overlay, and could ride on a silent allowance payment. It is now a row,
and a payment carrying data always asks. A 32-byte-hash cashaddr was
forced into a 20-byte template, producing an unspendable script; it is
refused.
Non-Permit typed data was flagged only when its primaryType was on a
short list (Seaport, SafeTx, ...); any other order, relay or
account-abstraction type got a plain Sign button and no PIN. Every
signed field is now listed with its declared type, and a payload that
names an address other than the user's and the verifying contract
together with an amount, or carries raw bytes, gets the danger button
and the PIN; text-only payloads stay ordinary.
The encoder signed "false" as true, turned non-hex characters into zero
bytes, wrapped integers past their width (2^256+5 signed as 5) and
signed a non-array as an empty array, each while the overlay showed
the original value. Such input is now refused before any overlay.
Domain rows and the chain check use only the fields EIP712Domain
declares; others are labelled as not signed.
Solana: a ComputeBudget price the site added was paid on top of the
base fee but shown as "program ComputeB...: not decoded", so a
transaction could burn the balance in priority fees behind a plain
Sign button. The maximum network fee is now a row, and Assign, durable
nonces, Approve/ApproveChecked, SetAuthority, closing a token account
to someone else and very high fees get a warning and the danger button.
signAndSend also requires one signature slot per required signer.
Ethereum: only allowances of 2^255 and up counted as unlimited; 2^96
and up now does, and Permit2 approve, increaseApproval, NFT
safeTransferFrom and multicall are decoded. The estimate shown was
gas x the node's price even when the site set a far higher tip, which
is what is actually paid; it now uses base fee + the real tip (or the
full legacy gasPrice) and warns when the site's fee is far above the
network's or a fifth of the balance. A personal_sign over 32 raw bytes
is a hash a Safe or an order book will take as approval of something
unseen, so it gets the danger button and the PIN.
Tron: TRC10 sent along with a contract call (call_token_value) was
never read, contract types Aegis does not decode were shown by name as
if harmless, a truncated TRC20 call rendered as "undefined", and the
validity window was hidden. Those are now shown, flagged or refused;
the TronGrid draft check also refuses a memo, a permission id or an
expiration more than a day away.
Remember-me sealed the master password with whatever safeStorage
offered, which on Linux without a keyring is a constant key, i.e. the
password in the clear in the add-on store; and it stored any string
without checking it. It now uses the same keystore test as the PIN,
verifies the password with the vault first, and drops a blob sealed
under no real keystore. Settings says that remember-me leaves the
master password readable to anything running as the user, which the
PIN's TPM protection does not change.
Coin selection, change and the fee on the overlay came from the
Electrum server's listunspent values, and a legacy signature does not
commit to the value it spends. A server that under-reported a P2PKH
coin made Aegis sign away the difference as fee: a 1,000,000 sat coin
reported as 100,000 produced a transaction paying 901,180 sat while
the overlay said 1,180. Segwit v0 commits only to its own input, which
leaves the two-request variant open.
Every non-taproot input's previous transaction is now fetched, its txid
recomputed, and its output's value and script compared with the plan;
the fee of the finalized PSBT must equal the approved one.
mountWallet zeroed the vault root after mounting, but the WizardConnect
adapter kept a reference to that same buffer and read it again for
every new pairing's relay key. Every pairing made after mount therefore
got a Nostr identity derived from 32 zero bytes and the pairing URI
alone, so anyone who saw the URI (the QR, a script on the dapp page)
could read the relay traffic, xpubs included, and speak as the wallet.
The adapter now gets, and keeps, its own copy.
The signing overlay and the PIN request named the dapp by its own
userPrompt, so a dapp paired once could present itself as any site.
The pairing origin the host verified is now recorded per URI and shown
instead; the dapp's text is a quoted row with invisible and bidi
characters removed. One sign request per connection may be on screen
at a time, and revoking a site in Aegis ends its pairings too.
Theseus's own quick-unlock PIN had the same limit as Aegis's: once the
DPAPI seal is opened (as the user, or from a disk image plus the
Windows password) the 6-digit PIN falls to an offline search. The PIN
is now also the authorization value of a Platform Crypto Provider TPM
key whose secret is mixed into the wrapping key, so the chip's lockout
bounds guessing; lib/tpm-pin.cjs is the same module Aegis uses.
set() now refuses when there is no real OS keystore (Linux basic_text
included) instead of writing the blob in the clear, an unsealed record
from an older build is deleted, and Settings says what the PIN actually
protects against on this machine.
A 6-digit PIN behind PBKDF2 + DPAPI falls in minutes to anything that
can open DPAPI (malware running as the user, a disk image plus the
Windows password). The PIN is now also the authorization value of a
TPM key from the Microsoft Platform Crypto Provider; the key releases a
secret mixed with PBKDF2(pin), so the stored blob alone opens nothing
and the chip's own lockout (32 failures, then one per 10 minutes)
limits guesses however the blob was obtained. Reached through Windows
PowerShell's CNG classes with the PIN on stdin, so no native module.
Machines without a TPM keep the software PIN, and Settings now says
plainly what that protects against.
A PIN is no longer stored when there is no real OS keystore (including
Linux's basic_text backend, whose key is a constant); a pre-0.31 plain
blob is sealed or deleted and remembered, so the panel can tell the
user to change the master password if the profile was ever copied.
0.31 moved the PIN check into index.js and, with it, replaced the
15-minute lockout after five wrong PINs by "PIN off until the master
password", with new wording on every PIN screen. The user wants the
screens as they were. The wording, the lockout and the switch to the
master password are back; the check stays in index.js, so the lockout is
now enforced by the host and the panel still never sees the PIN blob.
After the lockout every further wrong PIN locks it again.
Built-in extensions can answer paths under a name they ship with, so
Pithos opens at pithos.sia/<user>/<drive>/<folder> instead of a
loopback address, signed in only while the vault is unlocked. A tab
playing sound is no longer frozen in the background, so music keeps
playing when you switch tabs. A widened left panel covers the page
instead of squeezing it and the right sidebar. The tab that was open
at close loads on launch. Bundles Pithos 0.3.10 (views, compact sidebar
layout, music player, updater footer, Account page) and Aegis 0.31.0
(the audit fixes).
Pithos needs its full-page app at pithos.sia/<user>/<drive>/<folder>
instead of a loopback address. A new site-route capability lets a
built-in add-on declare names in its manifest (siteRoutes) and register
a handler for them; the bns:// handler asks it first for every path but
the root, and a handler that returns nothing hands the request back to
the name's own site. Community add-ons cannot use it, since answering
for a name is impersonating it.
Restore left every tab dormant, the active one included, so a fresh launch
showed the right tab highlighted over an empty page until it was clicked,
which reads as a broken restore. The active tab now loads as soon as the
toolbar has painted; every other restored tab stays dormant until it is
activated, so launch still costs one page renderer.
PIN checked by the host with a hard five-guess limit, permits described
from what is signed, unreadable WizardConnect requests refused, PIN
clearances bound to their request, permissions re-read after the PIN
wait, and the Tron duplicate-field fix (also shipped alone as 0.30.1).
- send, sendToken and consolidate now require the wallet id the panel
reviewed them for; a missing id used to skip the check, and the Solana
token send sent none, so a selection change under the PIN pad sent from
another wallet.
- "1 0" was read as 10: a space inside an amount is now refused.
- A BCMR registry list saved before https was enforced is filtered on read,
and names lose the soft hyphen, Arabic letter mark, Mongolian vowel
separator, line/paragraph separators and Unicode tag characters too.
signAndSend took a permissions snapshot, waited up to two minutes for the
PIN, then wrote the snapshot back. Revoking the site meanwhile still let
the allowance payment go out and restored the revoked allowance, and any
other permission change made during the wait was lost. After the wait it
now re-reads permissions, refuses an allowance payment whose allowance was
revoked, replaced or no longer covers it, and changes only this origin's
sendTx.
Freezing background tabs stopped music and talks the moment you
switched to another tab, which no other browser does. A tab that is
audible when its freeze is due is checked again later and frozen only
once it falls quiet.
One global 90 s clearance was opened by every PIN proof. Opening the
wallet or ticking a setting let the next dapp transaction from any site
through without a PIN; a dapp waiting in its poll could take the clearance
the user had just made for their own send; and with two sites waiting the
second one's request was dropped.
Each waiting dapp transaction now has an id, and only a proof naming that
id releases it; a proof given for "transaction" in the panel clears the
panel's next send only; any other proof clears nothing. The panel answers
waiting sites one at a time. Promote to HD now asks for the PIN like any
other spend instead of failing when PIN-per-transaction is on.
wc-sign accepted a flat request (tx and sourceOutputs at the top) that
buildWcApproval does not read, so any paired dapp could get a signature
after an overlay showing only the wallet, an input count and the sighash.
The signer now takes only the nested WizardConnect shape, and the overlay
refuses, without showing anything, a request whose outputs or spent
inputs it cannot decode. Total out now sums every output, not the first 8.
describePermit read td.message directly, and the EIP-712 encoder ignores
keys a type does not declare. A dapp could put a decoy allowed:false or
details:{amount:"1"} beside an unlimited permit and the overlay showed
the decoy, not risky, under the plain Sign button, with the same digest.
The overlay and the message preview are now built from the declared
fields only, the permit variant is picked from the declared type, and
dropped fields are counted on the overlay. Amounts of 2^96 units and up
are flagged as effectively unlimited, marketplace orders and Safe
transactions get the warning too, and the encoder refuses a non-hex
address instead of signing it as zero bytes.
The panel fetched the PIN blob and decrypted it itself, then reported its
own failures. The lockout therefore counted only what a well-behaved panel
chose to report, a 15-minute timer handed out five more guesses forever,
and anything able to run in the panel could take the blob and search the
million PINs offline in minutes.
The blob now never leaves index.js: pinSet builds it after checking the
master password against the vault, pinUnwrap counts each guess before
trying it, and five wrong guesses switch the PIN off until the master
password is entered. The panel keeps its PIN pads and only sends digits.
A blob from an older build (200k iterations) is re-made at 600k under a
fresh salt on the next correct PIN. Only the topmost PIN pad listens to
typed digits, so two stacked pads cannot both take one entry.
Widening an add-on's left panel shrank the open tab to a 120 px strip
and, with the right sidebar open, pushed the sidebar narrower too. Now
the widened panel lies over the tab area like a page of its own, up to
the right sidebar, which keeps its width; the tab underneath keeps its
size. Choosing a tab, opening Settings or a new tab, or using the
address bar narrows the panel back to a bar beside the page, the way a
maximized right sidebar already steps back.
0.30.0 is on the update channel and bundled in Theseus 0.3.75 with a Tron
decoder that a hostile site can use to show one transfer and sign another.
This is 0.30.0 plus that fix only; 0.31.0 is still under review.
The decoder read the first copy of a singular protobuf field; java-tron
keeps the last. Any.value is opaque bytes, so a TransferContract carrying
two recipients and two amounts hashes to the same txid either way: the
overlay showed "1 TRX to X" while the chain would move 999 TRX to
another address. It also defeated the plan-time and sign-time draft checks
against a hostile node. A repeated singular field, or a known field with
the wrong wire type, now refuses the transaction.
The decoder read the first copy of a singular protobuf field; java-tron
keeps the last. Any.value is opaque bytes, so a TransferContract carrying
two recipients and two amounts hashes to the same txid either way: the
overlay showed "1 TRX to X" while the chain would move 999 TRX to
another address. It also defeated the plan-time and sign-time draft checks
against a hostile node. A repeated singular field, or a known field with
the wrong wire type, now refuses the transaction.
- Page-inject policy. An add-on may keep { mode, origins } under the
reserved storage key "__pageInjectPolicy"; in "allowed" mode its bridge is
injected, and its page messages accepted, only on the listed origins. It
is read from the store because the decision is made synchronously at
document start and must hold while an on-demand add-on is still dormant.
A wallet injected into every page tells every page the user has one.
api.features.pageInjectPolicy lets an add-on tell an old host apart.
- vault.imports (raw imported seeds and keys, not namespaced per add-on) and
vault.lifecycle unlock/setup/lock (the master password, and an
unthrottled oracle for it) are for add-ons that ship with Theseus. A
community extension with vault-derive could read the wallet's imported
keys. Others use vault.requestUnlock, where Theseus draws the prompt.