Compare commits
2 Commits
45c5f31971
...
cc89cb891b
| Author | SHA1 | Date | |
|---|---|---|---|
| cc89cb891b | |||
| 19cb1ca1b0 |
43
README.md
43
README.md
@@ -107,12 +107,13 @@ unavailable). The suite lives in `tests/e2e/` and is driven by
|
|||||||
`playwright-core`, whose version must stay matched to the container's Playwright
|
`playwright-core`, whose version must stay matched to the container's Playwright
|
||||||
version — the browsers ship inside the image.
|
version — the browsers ship inside the image.
|
||||||
|
|
||||||
It covers popup load, wallet creation through the UI, the Add Token screen and
|
It covers popup load, WebAssembly compilation under the shipped CSP (see
|
||||||
the transaction detail screen for an ERC-20 transfer. All outbound network is
|
[Content Security Policy](#content-security-policy)), wallet creation through
|
||||||
intercepted at the browser level and served from fixtures in
|
the UI, the Add Token screen and the transaction detail screen for an ERC-20
|
||||||
`tests/e2e/network.js`, so the run is deterministic and fully offline;
|
transfer. All outbound network is intercepted at the browser level and served
|
||||||
unrecognised outbound requests are reported as failures rather than silently
|
from fixtures in `tests/e2e/network.js`, so the run is deterministic and fully
|
||||||
allowed.
|
offline; unrecognised outbound requests are reported as failures rather than
|
||||||
|
silently allowed.
|
||||||
|
|
||||||
That reporting has one bound worth knowing. Observation ends when the browser
|
That reporting has one bound worth knowing. Observation ends when the browser
|
||||||
context is torn down, and nothing can watch traffic after that, so the run keeps
|
context is torn down, and nothing can watch traffic after that, so the run keeps
|
||||||
@@ -751,6 +752,36 @@ battle-tested.
|
|||||||
Exceptions require explicit authorization in a code comment referencing this
|
Exceptions require explicit authorization in a code comment referencing this
|
||||||
policy, but as of now there are none.
|
policy, but as of now there are none.
|
||||||
|
|
||||||
|
### Content Security Policy
|
||||||
|
|
||||||
|
Both manifests declare the same policy for extension pages —
|
||||||
|
`script-src 'self' 'wasm-unsafe-eval'; object-src 'self'` — as an object under
|
||||||
|
`content_security_policy.extension_pages` in `manifest/chrome.json` (MV3) and as
|
||||||
|
a bare string in `manifest/firefox.json` (MV2).
|
||||||
|
|
||||||
|
`'wasm-unsafe-eval'` is there for one reason: libsodium. It ships a WebAssembly
|
||||||
|
build and a `wasm2js` translation of it in one file, tries WASM first, and
|
||||||
|
silently falls back to the translation if instantiation throws. Under a plain
|
||||||
|
`script-src 'self'` the fallback was taken on every popup load, announced by
|
||||||
|
nothing but an uncaught `CompileError`. Measured on the same Argon2id parameters
|
||||||
|
the vault uses (`OPSLIMIT_INTERACTIVE`, `MEMLIMIT_INTERACTIVE`), WASM derives a
|
||||||
|
key in 141-198ms and `wasm2js` in 3204-3660ms. The work factor is identical — it
|
||||||
|
is set by the ops and memory parameters, not by wall time — so the fallback
|
||||||
|
bought nothing and cost about three and a half seconds on every operation that
|
||||||
|
asks for the password, which is every signature.
|
||||||
|
|
||||||
|
The keyword permits compiling WebAssembly and nothing else: not `eval()` of
|
||||||
|
strings, not inline script, not remote script. Using it requires already
|
||||||
|
executing script in an extension page, which is complete compromise on its own.
|
||||||
|
`'unsafe-eval'` is a different proposition and is not granted.
|
||||||
|
|
||||||
|
The grant is pinned in both directions. `tests/manifest.test.js` asserts the
|
||||||
|
exact token set in both manifests, so dropping `'wasm-unsafe-eval'` (a silent
|
||||||
|
20x regression on the key derivation) and adding anything beyond it both fail
|
||||||
|
`make check`. `tests/vaultBackend.test.js` asserts the unit tests run the WASM
|
||||||
|
backend, and `make test-e2e` compiles a WebAssembly module inside the real popup
|
||||||
|
under the real manifest.
|
||||||
|
|
||||||
### DEBUG Mode Policy
|
### DEBUG Mode Policy
|
||||||
|
|
||||||
The `DEBUG` constant in the popup JS enables a red "DEBUG / INSECURE" banner and
|
The `DEBUG` constant in the popup JS enables a red "DEBUG / INSECURE" banner and
|
||||||
|
|||||||
8
TODO.md
8
TODO.md
@@ -44,6 +44,14 @@ undefined identifiers, which is how
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-08-11: libsodium runs on WebAssembly in the shipped builds —
|
||||||
|
`'wasm-unsafe-eval'` added to both manifest CSPs after measuring the wasm2js
|
||||||
|
fallback at 20x the Argon2id cost, pinned in both directions by
|
||||||
|
`tests/manifest.test.js` and observed in the real popup by the e2e suite
|
||||||
|
([#182](https://git.eeqj.de/sneak/AutistMask/issues/182)).
|
||||||
|
- 2026-08-11: `docs/README.md` rewritten against the code: no competitor names,
|
||||||
|
all five network destinations documented, password/Settings/Add Wallet
|
||||||
|
sections corrected ([#163](https://git.eeqj.de/sneak/AutistMask/issues/163)).
|
||||||
- 2026-08-11: Wallet deletion repairs its own state — `hasWallet` follows the
|
- 2026-08-11: Wallet deletion repairs its own state — `hasWallet` follows the
|
||||||
remaining wallets, the selection only moves when it was deleted, and the
|
remaining wallets, the selection only moves when it was deleted, and the
|
||||||
active-address change is broadcast to connected sites
|
active-address change is broadcast to connected sites
|
||||||
|
|||||||
239
docs/README.md
239
docs/README.md
@@ -6,10 +6,10 @@ and ERC-20 tokens, and connects to web3 sites. Nothing else.
|
|||||||
|
|
||||||
## Why AutistMask Exists
|
## Why AutistMask Exists
|
||||||
|
|
||||||
MetaMask has become bloated with swap UIs, portfolio dashboards, analytics,
|
The most popular browser-based EVM wallet has become bloated with swap UIs,
|
||||||
tracking, and advertisements. It is no longer a simple wallet. Most alternatives
|
portfolio dashboards, analytics, tracking, and advertisements. It is no longer a
|
||||||
(Rabby, Rainbow, etc.) only support Chromium browsers, leaving Firefox users
|
simple wallet. The common alternatives only support Chromium browsers, leaving
|
||||||
without a usable option.
|
Firefox users without a usable option.
|
||||||
|
|
||||||
AutistMask exists because a wallet should be a wallet. You should be able to see
|
AutistMask exists because a wallet should be a wallet. You should be able to see
|
||||||
your balances, send tokens, receive tokens, and connect to sites. That is all a
|
your balances, send tokens, receive tokens, and connect to sites. That is all a
|
||||||
@@ -27,9 +27,10 @@ analytics, use a portfolio tracker. The wallet is not the place for any of that.
|
|||||||
|
|
||||||
- **Encrypt your recovery phrase and private keys at rest.** Your secrets are
|
- **Encrypt your recovery phrase and private keys at rest.** Your secrets are
|
||||||
encrypted on disk using Argon2id key derivation and XSalsa20-Poly1305
|
encrypted on disk using Argon2id key derivation and XSalsa20-Poly1305
|
||||||
authenticated encryption (via libsodium). Your password is required only when
|
authenticated encryption (via libsodium). Your password is required whenever a
|
||||||
signing a transaction. Viewing balances and addresses never requires a
|
secret has to be decrypted: signing a transaction, signing a message or typed
|
||||||
password.
|
data, exporting a private key, and deleting a wallet. Viewing balances and
|
||||||
|
addresses never requires a password.
|
||||||
|
|
||||||
- **Let you choose your own RPC endpoint.** The default is a public Ethereum
|
- **Let you choose your own RPC endpoint.** The default is a public Ethereum
|
||||||
RPC, but you can point it at your own node or any provider you trust. No
|
RPC, but you can point it at your own node or any provider you trust. No
|
||||||
@@ -56,23 +57,25 @@ analytics, use a portfolio tracker. The wallet is not the place for any of that.
|
|||||||
|
|
||||||
- **No NFT galleries or portfolio views.** This is a wallet, not a dashboard.
|
- **No NFT galleries or portfolio views.** This is a wallet, not a dashboard.
|
||||||
|
|
||||||
- **No token auto-discovery.** AutistMask does not scan the blockchain for
|
- **No third-party token list APIs.** Token balances come from the same block
|
||||||
tokens you might hold. You add tokens manually by contract address. This
|
explorer you configure for transaction history, and the extension ships its
|
||||||
prevents scam tokens from appearing in your wallet uninvited.
|
own hardcoded list of top ERC-20 contract addresses for symbol-spoofing
|
||||||
|
detection. Any token you want tracked across all your addresses, you add
|
||||||
|
yourself by contract address.
|
||||||
|
|
||||||
- **No phishing blocklists from third parties.** AutistMask does not phone home
|
- **No backend servers operated by the developer.** Nothing is sent to any
|
||||||
to check URLs against a remote blocklist. It does maintain a local list of
|
server run by AutistMask. Every network destination is listed below.
|
||||||
known scam addresses, but this is shipped with the extension, not fetched from
|
|
||||||
a server.
|
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
AutistMask is a browser extension that runs entirely in your browser. It does
|
AutistMask is a browser extension that runs entirely in your browser. It does
|
||||||
not have a backend server. It communicates with three external services:
|
not have a backend server. It communicates with five external destinations:
|
||||||
|
three you configure yourself, and two fixed ones used for scam detection.
|
||||||
|
|
||||||
### External Services
|
### External Services
|
||||||
|
|
||||||
**Ethereum JSON-RPC endpoint** (default: `ethereum-rpc.publicnode.com`)
|
**Ethereum JSON-RPC endpoint** (default: `ethereum-rpc.publicnode.com`;
|
||||||
|
`ethereum-sepolia-rpc.publicnode.com` on Sepolia)
|
||||||
|
|
||||||
This is how AutistMask talks to the Ethereum network. Every wallet needs an
|
This is how AutistMask talks to the Ethereum network. Every wallet needs an
|
||||||
Ethereum node to check balances, estimate gas, broadcast transactions, and
|
Ethereum node to check balances, estimate gas, broadcast transactions, and
|
||||||
@@ -80,27 +83,73 @@ verify confirmations. The default is a free public RPC endpoint. You can change
|
|||||||
this in Settings to any Ethereum JSON-RPC endpoint, including your own local
|
this in Settings to any Ethereum JSON-RPC endpoint, including your own local
|
||||||
node.
|
node.
|
||||||
|
|
||||||
What gets sent: standard Ethereum JSON-RPC requests (balance queries,
|
When it is contacted: on every balance refresh (every 10 seconds while the popup
|
||||||
transaction broadcasts, gas estimates, ENS lookups). Your addresses are
|
is open, every 60 seconds in the background), when you type an ENS name into the
|
||||||
necessarily visible to the RPC provider when querying balances.
|
Send screen, when a send is prepared and broadcast, while a pending transaction
|
||||||
|
is polled for its receipt, and for the reverse ENS lookups used to label
|
||||||
|
addresses (cached for 12 hours).
|
||||||
|
|
||||||
**Blockscout API** (default: `eth.blockscout.com/api/v2`)
|
What gets sent: standard Ethereum JSON-RPC requests (balance queries,
|
||||||
|
transaction broadcasts, gas estimates, ENS lookups, contract-code checks). Your
|
||||||
|
addresses are necessarily visible to the RPC provider when querying balances.
|
||||||
|
|
||||||
|
**Blockscout API** (default: `eth.blockscout.com/api/v2`;
|
||||||
|
`eth-sepolia.blockscout.com/api/v2` on Sepolia)
|
||||||
|
|
||||||
Used to fetch token balances and transaction history. Blockscout is an
|
Used to fetch token balances and transaction history. Blockscout is an
|
||||||
open-source blockchain explorer. AutistMask queries it for your ERC-20 token
|
open-source blockchain explorer. AutistMask queries it for your ERC-20 token
|
||||||
balances and recent transactions. You can change this in Settings to a
|
balances (including the holder counts used for spam filtering) and your recent
|
||||||
|
transactions and token transfers. You can change this in Settings to a
|
||||||
self-hosted Blockscout instance.
|
self-hosted Blockscout instance.
|
||||||
|
|
||||||
|
When it is contacted: on every balance refresh, and whenever a screen showing
|
||||||
|
transaction history is opened.
|
||||||
|
|
||||||
What gets sent: your Ethereum addresses (to look up balances and transactions).
|
What gets sent: your Ethereum addresses (to look up balances and transactions).
|
||||||
|
|
||||||
**CoinDesk CADLI price API** (`data-api.coindesk.com`)
|
**CoinDesk CADLI price API** (`data-api.coindesk.com`)
|
||||||
|
|
||||||
Used to fetch current USD prices for ETH and ERC-20 tokens. Prices are cached
|
Used to fetch current USD prices for ETH and the top 25 tokens. Prices are
|
||||||
for 5 minutes. No API key is required. No user data is sent -- only a list of
|
cached for 5 minutes. No API key is required. This endpoint is not
|
||||||
token symbols (e.g. "ETH", "USDC") to get their prices.
|
user-configurable, and it is not contacted at all while you are on a testnet,
|
||||||
|
where no USD values are shown.
|
||||||
|
|
||||||
What gets sent: token symbol names. No addresses, no balances, no identifying
|
When it is contacted: while the popup is open, at most once every 5 minutes.
|
||||||
information.
|
|
||||||
|
What gets sent: token symbol names (e.g. "ETH", "USDC"). No addresses, no
|
||||||
|
balances, no identifying information. As with any request, CoinDesk sees your IP
|
||||||
|
address.
|
||||||
|
|
||||||
|
**Phishing domain blocklist** (`raw.githubusercontent.com`)
|
||||||
|
|
||||||
|
A community-maintained list of phishing domains, used to warn you when a site
|
||||||
|
that asks to connect, or to have a transaction or signature approved, is a known
|
||||||
|
scam. A copy is bundled into the extension at build time, so the protection
|
||||||
|
works before any network request happens. At runtime the extension fetches the
|
||||||
|
live list to pick up newly added domains, keeping only the entries not already
|
||||||
|
in the bundled copy (persisted locally if under 256 KiB). This endpoint is not
|
||||||
|
user-configurable.
|
||||||
|
|
||||||
|
When it is contacted: once when the background script starts, and every 24 hours
|
||||||
|
after that. It is a plain download of a public file — nothing about you is sent,
|
||||||
|
but the host sees your IP address. If the fetch fails, the bundled copy is still
|
||||||
|
used.
|
||||||
|
|
||||||
|
**Etherscan address labels** (`etherscan.io`; `sepolia.etherscan.io` on Sepolia)
|
||||||
|
|
||||||
|
When you review a send, AutistMask fetches the recipient's public Etherscan
|
||||||
|
address page and looks for a "Fake_Phishing"/"Phish/Hack" label or a scam
|
||||||
|
warning, and shows a red warning if it finds one. This is a plain page fetch
|
||||||
|
with no API key, made by your browser. It is best-effort: if it fails, it is
|
||||||
|
silently ignored. This endpoint is not user-configurable.
|
||||||
|
|
||||||
|
When it is contacted: each time you reach the send confirmation screen.
|
||||||
|
|
||||||
|
What gets sent: the recipient address you are about to send to, and your IP
|
||||||
|
address. Your own addresses are not sent.
|
||||||
|
|
||||||
|
Etherscan links shown elsewhere in the UI (on addresses, transactions, and token
|
||||||
|
contracts) are ordinary links. They contact nothing until you click them.
|
||||||
|
|
||||||
### What Stays Local
|
### What Stays Local
|
||||||
|
|
||||||
@@ -123,8 +172,11 @@ word recovery phrase can restore your wallet on any device without your
|
|||||||
password. The password only protects the copy stored in this browser. If you
|
password. The password only protects the copy stored in this browser. If you
|
||||||
lose your recovery phrase, your password cannot help you recover it.
|
lose your recovery phrase, your password cannot help you recover it.
|
||||||
|
|
||||||
Your password is only requested when you send a transaction. Viewing balances,
|
Your password is requested whenever an encrypted secret must be decrypted: when
|
||||||
receiving funds, and browsing transaction history never require your password.
|
you send a transaction, when a site asks you to sign a message or typed data,
|
||||||
|
when you export an address's private key, and when you delete a wallet. Viewing
|
||||||
|
balances, receiving funds, and browsing transaction history never require your
|
||||||
|
password.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -147,34 +199,46 @@ receiving funds, and browsing transaction history never require your password.
|
|||||||
### Creating a New Wallet
|
### Creating a New Wallet
|
||||||
|
|
||||||
1. Click the AutistMask icon in your browser toolbar.
|
1. Click the AutistMask icon in your browser toolbar.
|
||||||
2. Click "Add wallet".
|
2. Click "Add wallet" (on first use), or open Settings and click "+ Add wallet".
|
||||||
3. Click the die button to generate a random 12-word recovery phrase.
|
3. On the "From Phrase" tab, click the die button to generate a random 12-word
|
||||||
|
recovery phrase.
|
||||||
4. **Write down the recovery phrase and store it safely.** Anyone with these
|
4. **Write down the recovery phrase and store it safely.** Anyone with these
|
||||||
words can take your funds. If you lose them, your wallet is gone. AutistMask
|
words can take your funds. If you lose them, your wallet is gone. AutistMask
|
||||||
cannot recover them for you.
|
cannot recover them for you.
|
||||||
5. Choose a password. This encrypts your recovery phrase on this device.
|
5. Choose a password and confirm it. This encrypts your recovery phrase on this
|
||||||
6. Click "Add".
|
device.
|
||||||
|
6. Click "Import".
|
||||||
|
|
||||||
### Importing an Existing Wallet
|
### Importing an Existing Wallet
|
||||||
|
|
||||||
**From a recovery phrase:** Follow the same steps as creating a wallet, but
|
The Add Wallet screen has three tabs:
|
||||||
paste your existing 12 or 24 word recovery phrase instead of generating a new
|
|
||||||
one. AutistMask uses the same derivation path as MetaMask (`m/44'/60'/0'/0`), so
|
|
||||||
your addresses will match.
|
|
||||||
|
|
||||||
**From a private key:** On the Add Wallet screen, click "Have a private key
|
**From Phrase:** Paste your existing 12 or 24 word recovery phrase instead of
|
||||||
instead?" and paste your private key. This creates a single-address wallet.
|
generating a new one. AutistMask uses the standard BIP-44 Ethereum derivation
|
||||||
|
path (`m/44'/60'/0'/0`), which is what other wallets use by default, so your
|
||||||
|
addresses will match and your phrase stays portable in both directions.
|
||||||
|
|
||||||
|
**From Key:** Paste a single private key. This creates a single-address wallet.
|
||||||
|
|
||||||
|
**From xprv:** Paste an extended private key. This imports the HD wallet and
|
||||||
|
scans for used addresses.
|
||||||
|
|
||||||
|
All three tabs ask for the same password fields, and the "Import" button
|
||||||
|
finishes the job.
|
||||||
|
|
||||||
### Adding More Addresses
|
### Adding More Addresses
|
||||||
|
|
||||||
HD wallets (created from a recovery phrase) can derive multiple addresses. On
|
HD wallets (created from a recovery phrase or an xprv) can derive multiple
|
||||||
the home screen, click the "+" button next to a wallet name to add the next
|
addresses. On the home screen, click the "+" button next to a wallet name to add
|
||||||
address. These are deterministic -- the same recovery phrase will always produce
|
the next address. These are deterministic -- the same recovery phrase will
|
||||||
the same sequence of addresses.
|
always produce the same sequence of addresses.
|
||||||
|
|
||||||
### Adding ERC-20 Tokens
|
### Adding ERC-20 Tokens
|
||||||
|
|
||||||
AutistMask does not auto-discover tokens. To track a token:
|
Tokens you hold show up automatically only if they are in the extension's
|
||||||
|
bundled list of well-known tokens or have at least 1,000 holders; everything
|
||||||
|
else is treated as spam and hidden. To track a token explicitly (which also
|
||||||
|
shows it at zero balance), add it by contract address:
|
||||||
|
|
||||||
1. Go to an address detail view (click `[info]` on any address).
|
1. Go to an address detail view (click `[info]` on any address).
|
||||||
2. Click "+ Token".
|
2. Click "+ Token".
|
||||||
@@ -183,12 +247,13 @@ AutistMask does not auto-discover tokens. To track a token:
|
|||||||
4. Click "Add".
|
4. Click "Add".
|
||||||
|
|
||||||
The token balance will appear on the address detail screen and on the home
|
The token balance will appear on the address detail screen and on the home
|
||||||
screen.
|
screen. Tokens can also be added from Settings, under "Tracked Tokens".
|
||||||
|
|
||||||
## Sending
|
## Sending
|
||||||
|
|
||||||
1. Click "Send" from the home screen or an address detail view.
|
1. Click "Send" from the home screen or an address detail view.
|
||||||
2. Select what to send (ETH or any tracked ERC-20 token).
|
2. Select what to send (ETH, or any ERC-20 token with a balance on this address
|
||||||
|
that survives the spam filters).
|
||||||
3. Enter the recipient address or ENS name (e.g. `vitalik.eth`).
|
3. Enter the recipient address or ENS name (e.g. `vitalik.eth`).
|
||||||
4. Enter the amount.
|
4. Enter the amount.
|
||||||
5. Click "Review" to see the confirmation screen.
|
5. Click "Review" to see the confirmation screen.
|
||||||
@@ -201,11 +266,14 @@ The confirmation screen shows:
|
|||||||
- **Amount** with USD estimate
|
- **Amount** with USD estimate
|
||||||
- **Your current balance** with USD estimate
|
- **Your current balance** with USD estimate
|
||||||
- **Estimated network fee** in ETH with USD estimate
|
- **Estimated network fee** in ETH with USD estimate
|
||||||
|
- **Warnings** if the recipient is a contract, a burn address, one of your own
|
||||||
|
addresses, on the bundled scam-address list, or labelled as a phisher on
|
||||||
|
Etherscan
|
||||||
|
|
||||||
After reviewing, click "Send" and enter your password. The transaction will be
|
After reviewing, enter your password and click "Sign & Send". The transaction
|
||||||
broadcast to the network and you will see a waiting screen with a timer. Once
|
will be broadcast to the network and you will see a waiting screen with a timer.
|
||||||
confirmed (or after 60 seconds), you will see either a success or error screen
|
Once confirmed (or after 60 seconds), you will see either a success or error
|
||||||
with the transaction hash and an Etherscan link.
|
screen with the transaction hash and an Etherscan link.
|
||||||
|
|
||||||
### Sending a Specific Token
|
### Sending a Specific Token
|
||||||
|
|
||||||
@@ -219,10 +287,10 @@ cannot accidentally switch to a different one.
|
|||||||
1. Click "Receive" from the home screen or an address detail view.
|
1. Click "Receive" from the home screen or an address detail view.
|
||||||
2. Share the QR code or copy the address using the "Copy address" button.
|
2. Share the QR code or copy the address using the "Copy address" button.
|
||||||
|
|
||||||
When receiving ERC-20 tokens, make sure the sender is sending on the Ethereum
|
When receiving ERC-20 tokens, make sure the sender is sending on the network you
|
||||||
network. AutistMask is an Ethereum mainnet wallet. Tokens sent on other networks
|
are using. AutistMask supports Ethereum mainnet and the Sepolia testnet. Tokens
|
||||||
(Polygon, Arbitrum, BSC, etc.) to the same address will not appear and may be
|
sent on other networks (Polygon, Arbitrum, BSC, etc.) to the same address will
|
||||||
permanently lost.
|
not appear and may be permanently lost.
|
||||||
|
|
||||||
## Connecting to Web3 Sites
|
## Connecting to Web3 Sites
|
||||||
|
|
||||||
@@ -237,7 +305,12 @@ pages. When a site requests access to your wallet:
|
|||||||
|
|
||||||
When a connected site requests a transaction, a separate approval popup appears
|
When a connected site requests a transaction, a separate approval popup appears
|
||||||
showing the transaction details (from, to, value, data). You must enter your
|
showing the transaction details (from, to, value, data). You must enter your
|
||||||
password and click "Confirm" to authorize it.
|
password and click "Confirm" to authorize it. Message and typed-data signature
|
||||||
|
requests work the same way, with a "Sign" button, and also require your
|
||||||
|
password.
|
||||||
|
|
||||||
|
If the requesting site's domain is on the phishing blocklist, all three approval
|
||||||
|
screens show a red phishing warning before you decide.
|
||||||
|
|
||||||
You can manage site permissions in Settings. Allowed and denied sites can be
|
You can manage site permissions in Settings. Allowed and denied sites can be
|
||||||
individually removed to reset their permissions.
|
individually removed to reset their permissions.
|
||||||
@@ -247,15 +320,16 @@ individually removed to reset their permissions.
|
|||||||
AutistMask includes several defenses against common Ethereum scams, all enabled
|
AutistMask includes several defenses against common Ethereum scams, all enabled
|
||||||
by default:
|
by default:
|
||||||
|
|
||||||
**Known token symbol verification.** AutistMask ships a list of ~250 legitimate
|
**Known token symbol verification.** AutistMask ships a list of roughly 500
|
||||||
ERC-20 tokens with their contract addresses. If a transaction claims to involve
|
legitimate ERC-20 tokens with their contract addresses. If a transaction or
|
||||||
a known symbol (like "ETH" or "USDT") but comes from an unrecognized contract,
|
balance claims to involve a known symbol (like "ETH" or "USDT") but comes from
|
||||||
it is identified as a spoof and hidden.
|
an unrecognized contract, it is identified as a spoof and hidden.
|
||||||
|
|
||||||
**Low-holder token filtering.** Tokens with fewer than 1,000 holders are hidden
|
**Low-holder token filtering.** Tokens with fewer than 1,000 holders are hidden
|
||||||
from transaction history and the send token list. Legitimate tokens have
|
from transaction history and the send token list, and are left out of your
|
||||||
substantial holder counts; scam tokens deployed for address poisoning typically
|
balances unless they are on the bundled known-token list or you added them
|
||||||
have zero.
|
yourself. Legitimate tokens have substantial holder counts; scam tokens deployed
|
||||||
|
for address poisoning typically have zero.
|
||||||
|
|
||||||
**Fraud contract blocklist.** When AutistMask detects a fraudulent transfer, it
|
**Fraud contract blocklist.** When AutistMask detects a fraudulent transfer, it
|
||||||
adds the contract address to a local blocklist. Future transactions from that
|
adds the contract address to a local blocklist. Future transactions from that
|
||||||
@@ -266,31 +340,47 @@ ETH by default) are hidden. Scammers send dust from look-alike addresses to
|
|||||||
plant them in your transaction history. The threshold is configurable in
|
plant them in your transaction history. The threshold is configurable in
|
||||||
Settings.
|
Settings.
|
||||||
|
|
||||||
All of these filters can be individually disabled in Settings if you prefer to
|
**Scam address list.** A list of known fraud, drainer, and phishing addresses is
|
||||||
|
shipped with the extension. Sending to one of them raises a warning on the
|
||||||
|
confirmation screen. It contains only addresses involved in fraud -- it is not a
|
||||||
|
sanctions list.
|
||||||
|
|
||||||
|
**Phishing domain warnings.** Sites asking to connect or to have something
|
||||||
|
approved are checked against the phishing domain blocklist described under
|
||||||
|
External Services, and flagged with a red banner if they match.
|
||||||
|
|
||||||
|
The first four filters can be individually disabled in Settings if you prefer to
|
||||||
see everything unfiltered.
|
see everything unfiltered.
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
Click the gear icon on the home screen to access settings:
|
Click the gear icon on the home screen to access settings:
|
||||||
|
|
||||||
- **Wallets**: Add a new wallet.
|
- **Wallets**: Your wallets, and "+ Add wallet".
|
||||||
- **Display**: Toggle whether tracked tokens with zero balance are shown.
|
- **Tracked Tokens**: The ERC-20 tokens tracked across all addresses, and "+ Add
|
||||||
|
token".
|
||||||
|
- **Display**: Toggle whether tracked tokens with zero balance are shown, and
|
||||||
|
choose the theme (System, Light, or Dark).
|
||||||
|
- **Network**: Switch between Ethereum Mainnet and Sepolia Testnet. Switching
|
||||||
|
resets the RPC and Blockscout endpoints to that network's defaults.
|
||||||
- **Ethereum RPC**: Change the Ethereum node endpoint. Default is a public RPC.
|
- **Ethereum RPC**: Change the Ethereum node endpoint. Default is a public RPC.
|
||||||
You can use your own node for maximum privacy.
|
You can use your own node for maximum privacy.
|
||||||
- **Blockscout API**: Change the Blockscout instance used for token balances and
|
- **Blockscout API**: Change the Blockscout instance used for token balances and
|
||||||
transaction history. You can use a self-hosted instance.
|
transaction history. You can use a self-hosted instance.
|
||||||
- **Token Spam Protection**: Toggle individual scam filters and set the dust
|
- **Token Spam Protection**: Toggle individual scam filters, set the dust
|
||||||
transaction threshold.
|
transaction threshold, and switch timestamps to UTC.
|
||||||
- **Allowed Sites / Denied Sites**: View and manage web3 site permissions.
|
- **Allowed Sites / Denied Sites**: View and manage web3 site permissions.
|
||||||
|
- **About**: License, author, version, release date, and a link to the commit
|
||||||
|
this build came from.
|
||||||
|
|
||||||
## Frequently Asked Questions
|
## Frequently Asked Questions
|
||||||
|
|
||||||
**Is AutistMask compatible with MetaMask?**
|
**Can I use AutistMask alongside another wallet?**
|
||||||
|
|
||||||
Yes. AutistMask uses the same derivation path (`m/44'/60'/0'/0`) as MetaMask. If
|
Yes. AutistMask uses the standard `m/44'/60'/0'/0` derivation path, so importing
|
||||||
you import the same recovery phrase, you will get the same addresses. You can
|
the same recovery phrase gives you the same addresses as any other wallet using
|
||||||
use both wallets side by side, though only one can be the active
|
that path. Two wallet extensions can be installed side by side, though only one
|
||||||
`window.ethereum` provider at a time.
|
can be the active `window.ethereum` provider at a time.
|
||||||
|
|
||||||
**Can I use AutistMask with a hardware wallet?**
|
**Can I use AutistMask with a hardware wallet?**
|
||||||
|
|
||||||
@@ -298,8 +388,9 @@ Not yet. Hardware wallet support may be added in the future.
|
|||||||
|
|
||||||
**Does AutistMask support networks other than Ethereum mainnet?**
|
**Does AutistMask support networks other than Ethereum mainnet?**
|
||||||
|
|
||||||
Not currently. AutistMask is Ethereum mainnet only. Multi-chain support may be
|
Ethereum mainnet and the Sepolia testnet, selectable in Settings. No other
|
||||||
added in the future.
|
networks are supported today. On Sepolia, USD values are not shown, because
|
||||||
|
testnet tokens have no market value.
|
||||||
|
|
||||||
**Where is my data stored?**
|
**Where is my data stored?**
|
||||||
|
|
||||||
@@ -312,7 +403,7 @@ to any server operated by AutistMask.
|
|||||||
|
|
||||||
Your data is deleted. Make sure you have your recovery phrase backed up before
|
Your data is deleted. Make sure you have your recovery phrase backed up before
|
||||||
uninstalling. With your recovery phrase, you can restore your wallet in
|
uninstalling. With your recovery phrase, you can restore your wallet in
|
||||||
AutistMask or any other compatible wallet (MetaMask, etc.) at any time.
|
AutistMask or any other wallet that uses the standard derivation path.
|
||||||
|
|
||||||
**What happens if a transaction times out?**
|
**What happens if a transaction times out?**
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
"description": "Minimal Ethereum wallet for Chrome",
|
"description": "Minimal Ethereum wallet for Chrome",
|
||||||
"permissions": ["storage", "activeTab"],
|
"permissions": ["storage", "activeTab"],
|
||||||
"host_permissions": ["<all_urls>"],
|
"host_permissions": ["<all_urls>"],
|
||||||
|
"content_security_policy": {
|
||||||
|
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
|
||||||
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"default_popup": "src/popup/index.html"
|
"default_popup": "src/popup/index.html"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Minimal Ethereum wallet for Firefox",
|
"description": "Minimal Ethereum wallet for Firefox",
|
||||||
"permissions": ["storage", "activeTab", "<all_urls>"],
|
"permissions": ["storage", "activeTab", "<all_urls>"],
|
||||||
|
"content_security_policy": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'",
|
||||||
"browser_action": {
|
"browser_action": {
|
||||||
"default_popup": "src/popup/index.html"
|
"default_popup": "src/popup/index.html"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,14 +1,75 @@
|
|||||||
// Vault: password-based encryption of secrets using libsodium.
|
// Vault: password-based encryption of secrets using libsodium.
|
||||||
// Uses Argon2id for key derivation and XSalsa20-Poly1305 for encryption.
|
// Uses Argon2id for key derivation and XSalsa20-Poly1305 for encryption.
|
||||||
// All crypto operations are delegated to libsodium — no raw primitives.
|
// All crypto operations are delegated to libsodium — no raw primitives.
|
||||||
|
//
|
||||||
|
// Backend: WebAssembly, deliberately (#182).
|
||||||
|
//
|
||||||
|
// libsodium ships one file containing both a WebAssembly build and a
|
||||||
|
// wasm2js ("asm.js") translation of it. It tries WASM first and, if
|
||||||
|
// instantiation throws, silently swaps in the translation. An extension
|
||||||
|
// CSP of plain script-src 'self' refuses WASM, so every popup load used
|
||||||
|
// to take that fallback — announced by nothing but an uncaught
|
||||||
|
// CompileError in the console.
|
||||||
|
//
|
||||||
|
// Measured here, same Argon2id parameters (OPSLIMIT_INTERACTIVE,
|
||||||
|
// MEMLIMIT_INTERACTIVE = 2 passes over 64MiB), node 22 on this machine:
|
||||||
|
// WASM 141-198ms per derivation, wasm2js 3204-3660ms. The work factor is
|
||||||
|
// identical either way — it is set by the ops/mem parameters, not by wall
|
||||||
|
// time — so the fallback bought no security, it only made every password
|
||||||
|
// operation take three and a half seconds, and the wallet asks for the
|
||||||
|
// password on every signature.
|
||||||
|
//
|
||||||
|
// So both manifests declare 'wasm-unsafe-eval' for extension pages. That
|
||||||
|
// keyword permits compiling WebAssembly and nothing else: not eval() of
|
||||||
|
// strings, not inline script, not remote script. Reaching it requires
|
||||||
|
// already executing script in the extension page, which is total
|
||||||
|
// compromise on its own. 'unsafe-eval' would be a different matter and is
|
||||||
|
// not granted. tests/manifest.test.js pins both policies to exactly
|
||||||
|
// "'self' 'wasm-unsafe-eval'" so neither the grant nor the surrounding
|
||||||
|
// strictness can drift unnoticed.
|
||||||
|
//
|
||||||
|
// The fallback still exists, and a wallet that refuses to decrypt is
|
||||||
|
// worse than a slow one, so it is not disabled — it is made loud:
|
||||||
|
// cryptoBackend() reports which backend this realm can run, ensureReady()
|
||||||
|
// logs an error if it is not WASM, tests/vaultBackend.test.js asserts the
|
||||||
|
// unit tests exercise the WASM backend, and the end-to-end suite asserts
|
||||||
|
// it in the real popup under the real manifest.
|
||||||
|
|
||||||
const sodium = require("libsodium-wrappers-sumo");
|
const sodium = require("libsodium-wrappers-sumo");
|
||||||
|
const { log } = require("./log");
|
||||||
|
|
||||||
|
// An empty WebAssembly module: the 8-byte magic number and version header,
|
||||||
|
// no sections. Compiling it asks the cheapest possible form of the only
|
||||||
|
// question that matters here — may this realm compile WebAssembly at all —
|
||||||
|
// which is exactly what a CSP without 'wasm-unsafe-eval' refuses, and
|
||||||
|
// exactly what decides which backend libsodium ends up on.
|
||||||
|
const EMPTY_WASM_MODULE = new Uint8Array([
|
||||||
|
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// "wasm" or "asmjs": the libsodium backend in use in this realm.
|
||||||
|
async function cryptoBackend() {
|
||||||
|
try {
|
||||||
|
await WebAssembly.compile(EMPTY_WASM_MODULE);
|
||||||
|
return "wasm";
|
||||||
|
} catch (_) {
|
||||||
|
return "asmjs";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let ready = false;
|
let ready = false;
|
||||||
|
|
||||||
async function ensureReady() {
|
async function ensureReady() {
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
await sodium.ready;
|
await sodium.ready;
|
||||||
|
if ((await cryptoBackend()) !== "wasm") {
|
||||||
|
log.errorf(
|
||||||
|
"libsodium is running on the wasm2js fallback: this realm " +
|
||||||
|
"refuses to compile WebAssembly, so every password " +
|
||||||
|
"derivation costs roughly 20x what it should. See the " +
|
||||||
|
"backend note in src/shared/vault.js.",
|
||||||
|
);
|
||||||
|
}
|
||||||
ready = true;
|
ready = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,4 +120,4 @@ async function decryptWithPassword(encrypted, password) {
|
|||||||
return sodium.to_string(plaintext);
|
return sodium.to_string(plaintext);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { encryptWithPassword, decryptWithPassword };
|
module.exports = { cryptoBackend, decryptWithPassword, encryptWithPassword };
|
||||||
|
|||||||
@@ -22,18 +22,12 @@ const EXT_PATH = path.join(REPO_ROOT, "dist", "chrome");
|
|||||||
// entry must name the issue that will remove it. This list is the one
|
// entry must name the issue that will remove it. This list is the one
|
||||||
// concession in an otherwise zero-tolerance policy: an uncaught error is
|
// concession in an otherwise zero-tolerance policy: an uncaught error is
|
||||||
// how this harness caught issue #150 in the first place.
|
// how this harness caught issue #150 in the first place.
|
||||||
const ALLOWED_ERRORS = [
|
//
|
||||||
{
|
// Empty, and worth keeping that way. Its only entry was the WASM
|
||||||
// libsodium ships a WASM build and an asm.js fallback. The
|
// CompileError libsodium provoked on every popup load, deleted with #182
|
||||||
// extension CSP (script-src 'self', with no wasm-unsafe-eval)
|
// when both manifests started allowing WASM; the run that used to need it
|
||||||
// refuses the WASM module on every popup load; libsodium catches
|
// is now the run that proves the fix.
|
||||||
// it and falls back to asm.js, so the wallet works. Deciding
|
const ALLOWED_ERRORS = [];
|
||||||
// which backend actually ships is issue #182, and this entry gets
|
|
||||||
// deleted when that lands.
|
|
||||||
issue: "#182",
|
|
||||||
pattern: /Refused to compile or instantiate WebAssembly module/,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function isAllowed(text) {
|
function isAllowed(text) {
|
||||||
return ALLOWED_ERRORS.some((a) => a.pattern.test(text));
|
return ALLOWED_ERRORS.some((a) => a.pattern.test(text));
|
||||||
@@ -247,6 +241,26 @@ async function visible(page, selector, timeout = 15000) {
|
|||||||
await page.waitForSelector(selector, { state: "visible", timeout });
|
await page.waitForSelector(selector, { state: "visible", timeout });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An empty WebAssembly module: magic number and version header, no
|
||||||
|
// sections. Compiling it in the popup asks the one question that decides
|
||||||
|
// libsodium's backend — may this realm compile WebAssembly — of the real
|
||||||
|
// page under the real shipped manifest, which is the only place the
|
||||||
|
// answer can be observed. Kept independent of src/shared/vault.js on
|
||||||
|
// purpose: a bundle asked to grade itself proves less than an outside
|
||||||
|
// observation of the same realm.
|
||||||
|
const EMPTY_WASM_MODULE = [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
|
||||||
|
|
||||||
|
async function pageCompilesWasm(page) {
|
||||||
|
return page.evaluate(async (bytes) => {
|
||||||
|
try {
|
||||||
|
await WebAssembly.compile(new Uint8Array(bytes));
|
||||||
|
return true;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, EMPTY_WASM_MODULE);
|
||||||
|
}
|
||||||
|
|
||||||
async function openPopup(ctx, popupUrl) {
|
async function openPopup(ctx, popupUrl) {
|
||||||
const page = await ctx.newPage();
|
const page = await ctx.newPage();
|
||||||
await page.goto(popupUrl);
|
await page.goto(popupUrl);
|
||||||
@@ -285,5 +299,6 @@ module.exports = {
|
|||||||
launch,
|
launch,
|
||||||
openAddressDetail,
|
openAddressDetail,
|
||||||
openPopup,
|
openPopup,
|
||||||
|
pageCompilesWasm,
|
||||||
visible,
|
visible,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const {
|
|||||||
launch,
|
launch,
|
||||||
openAddressDetail,
|
openAddressDetail,
|
||||||
openPopup,
|
openPopup,
|
||||||
|
pageCompilesWasm,
|
||||||
visible,
|
visible,
|
||||||
} = require("./harness");
|
} = require("./harness");
|
||||||
const { STUB_TOKEN, STUB_TX_HASH } = require("./network");
|
const { STUB_TOKEN, STUB_TX_HASH } = require("./network");
|
||||||
@@ -55,6 +56,27 @@ test("popup loads and reaches the welcome view", async (env) => {
|
|||||||
assert(title === "AutistMask", "unexpected popup title: " + title);
|
assert(title === "AutistMask", "unexpected popup title: " + title);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The empirical half of #182. The manifest change is only a claim about
|
||||||
|
// what the CSP permits; this is the observation. Two things have to hold
|
||||||
|
// together, and the run covers both: the popup realm compiles WASM (here),
|
||||||
|
// and no WASM refusal or abort is recorded anywhere in the run — the
|
||||||
|
// harness allowlist that used to excuse exactly that error is now empty,
|
||||||
|
// so a recurrence fails whichever test it lands in rather than being
|
||||||
|
// tolerated. Since libsodium's WASM module is embedded in the bundle and
|
||||||
|
// needs no fetch, a realm that compiles WASM is a realm where libsodium
|
||||||
|
// takes the WASM path, and the next test drives a real vault encryption
|
||||||
|
// through it.
|
||||||
|
test("the popup compiles WebAssembly under the shipped CSP (#182)", async (env) => {
|
||||||
|
const ok = await pageCompilesWasm(env.page);
|
||||||
|
assert(
|
||||||
|
ok,
|
||||||
|
"the popup refused to compile WebAssembly. The shipped manifest CSP " +
|
||||||
|
"has lost 'wasm-unsafe-eval', so libsodium is back on its wasm2js " +
|
||||||
|
"fallback and every password derivation costs roughly 20x what it " +
|
||||||
|
"should — see the backend note in src/shared/vault.js",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("wallet creation through the UI reaches the main view", async (env) => {
|
test("wallet creation through the UI reaches the main view", async (env) => {
|
||||||
await createWallet(env.page);
|
await createWallet(env.page);
|
||||||
const addrCount = await env.page
|
const addrCount = await env.page
|
||||||
|
|||||||
100
tests/manifest.test.js
Normal file
100
tests/manifest.test.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
// The shipped Content Security Policy, pinned in both directions.
|
||||||
|
//
|
||||||
|
// This is the anti-regression check for #182. libsodium decides its
|
||||||
|
// backend by trying to compile WebAssembly and catching the failure, so a
|
||||||
|
// CSP that refuses WASM demotes the vault to the wasm2js translation —
|
||||||
|
// roughly 20x slower per Argon2id derivation — and says so only in a
|
||||||
|
// console message nobody reads. Dropping 'wasm-unsafe-eval' from either
|
||||||
|
// manifest therefore has to fail a check, not a log line.
|
||||||
|
//
|
||||||
|
// It is equally a check against loosening. 'wasm-unsafe-eval' is granted
|
||||||
|
// deliberately and narrowly (see the backend note in src/shared/vault.js);
|
||||||
|
// 'unsafe-eval', 'unsafe-inline' and any remote script source are not, and
|
||||||
|
// an exact match on the token set is what keeps the next edit from
|
||||||
|
// smuggling one in alongside.
|
||||||
|
//
|
||||||
|
// build.js copies these files to dist/<target>/manifest.json verbatim, so
|
||||||
|
// what is asserted here is what ships.
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const MANIFEST_DIR = path.join(__dirname, "..", "manifest");
|
||||||
|
|
||||||
|
const EXPECTED_SCRIPT_SRC = ["'self'", "'wasm-unsafe-eval'"];
|
||||||
|
const EXPECTED_OBJECT_SRC = ["'self'"];
|
||||||
|
|
||||||
|
const FORBIDDEN_SOURCES = [
|
||||||
|
"'unsafe-eval'",
|
||||||
|
"'unsafe-inline'",
|
||||||
|
"http:",
|
||||||
|
"https:",
|
||||||
|
"data:",
|
||||||
|
"blob:",
|
||||||
|
"*",
|
||||||
|
];
|
||||||
|
|
||||||
|
function readManifest(name) {
|
||||||
|
return JSON.parse(
|
||||||
|
fs.readFileSync(path.join(MANIFEST_DIR, name + ".json"), "utf8"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// "script-src 'self'; object-src 'self'" -> { "script-src": ["'self'"], ... }
|
||||||
|
function parseCsp(policy) {
|
||||||
|
const directives = {};
|
||||||
|
for (const part of policy.split(";")) {
|
||||||
|
const tokens = part.trim().split(/\s+/).filter(Boolean);
|
||||||
|
if (tokens.length === 0) continue;
|
||||||
|
directives[tokens[0]] = tokens.slice(1);
|
||||||
|
}
|
||||||
|
return directives;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertPolicy(policy) {
|
||||||
|
const directives = parseCsp(policy);
|
||||||
|
expect(Object.keys(directives).sort()).toEqual([
|
||||||
|
"object-src",
|
||||||
|
"script-src",
|
||||||
|
]);
|
||||||
|
expect(directives["script-src"].slice().sort()).toEqual(
|
||||||
|
EXPECTED_SCRIPT_SRC,
|
||||||
|
);
|
||||||
|
expect(directives["object-src"].slice().sort()).toEqual(
|
||||||
|
EXPECTED_OBJECT_SRC,
|
||||||
|
);
|
||||||
|
for (const source of FORBIDDEN_SOURCES) {
|
||||||
|
expect(directives["script-src"]).not.toContain(source);
|
||||||
|
expect(directives["object-src"]).not.toContain(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("shipped Content Security Policy", () => {
|
||||||
|
// MV3 takes an object and applies extension_pages to the popup and the
|
||||||
|
// background service worker, which is where libsodium runs.
|
||||||
|
test("chrome MV3 allows WASM and nothing else beyond 'self'", () => {
|
||||||
|
const csp = readManifest("chrome").content_security_policy;
|
||||||
|
expect(typeof csp).toBe("object");
|
||||||
|
expect(Object.keys(csp)).toEqual(["extension_pages"]);
|
||||||
|
assertPolicy(csp.extension_pages);
|
||||||
|
});
|
||||||
|
|
||||||
|
// MV2 takes the policy as a bare string, and Firefox 102 and later
|
||||||
|
// require 'wasm-unsafe-eval' for extension pages exactly as Chrome
|
||||||
|
// does. Same policy, different manifest shape.
|
||||||
|
test("firefox MV2 allows WASM and nothing else beyond 'self'", () => {
|
||||||
|
const csp = readManifest("firefox").content_security_policy;
|
||||||
|
expect(typeof csp).toBe("string");
|
||||||
|
assertPolicy(csp);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The two targets share one codebase and one crypto path; a policy
|
||||||
|
// that drifts apart between them means one of the two builds is
|
||||||
|
// running a backend nothing tests.
|
||||||
|
test("both targets ship the same policy", () => {
|
||||||
|
const chrome =
|
||||||
|
readManifest("chrome").content_security_policy.extension_pages;
|
||||||
|
const firefox = readManifest("firefox").content_security_policy;
|
||||||
|
expect(firefox).toBe(chrome);
|
||||||
|
});
|
||||||
|
});
|
||||||
52
tests/vaultBackend.test.js
Normal file
52
tests/vaultBackend.test.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// The unit tests must exercise the libsodium backend that actually ships
|
||||||
|
// (#182). Before this, they could not: node compiles WebAssembly happily,
|
||||||
|
// the extension CSP refused it, and so the browser silently ran the
|
||||||
|
// wasm2js translation while every test ran the WASM build.
|
||||||
|
//
|
||||||
|
// With 'wasm-unsafe-eval' in both manifests the two agree, and these tests
|
||||||
|
// hold that agreement in place from the node side. tests/manifest.test.js
|
||||||
|
// holds up the CSP end of it, and the end-to-end suite observes the real
|
||||||
|
// popup.
|
||||||
|
|
||||||
|
const { cryptoBackend } = require("../src/shared/vault");
|
||||||
|
|
||||||
|
// The module libsodium-wrappers-sumo itself requires and drives. Not a new
|
||||||
|
// dependency: it is inspected here, never used to perform crypto, because
|
||||||
|
// it is the only thing that can say which backend is loaded.
|
||||||
|
const SODIUM_CORE = "libsodium-sumo";
|
||||||
|
|
||||||
|
describe("libsodium backend", () => {
|
||||||
|
test("this realm compiles WebAssembly, so the tests run the WASM build", async () => {
|
||||||
|
await expect(cryptoBackend()).resolves.toBe("wasm");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("libsodium did not swap in the wasm2js fallback", async () => {
|
||||||
|
const core = require(SODIUM_CORE);
|
||||||
|
await require("libsodium-wrappers-sumo").ready;
|
||||||
|
// useBackupModule is the entry point to the fallback; taking it
|
||||||
|
// replaces the module's exports with the translation's, and the
|
||||||
|
// entry point goes with them. Still present after ready means the
|
||||||
|
// WASM module is the one in place. The test below is what keeps
|
||||||
|
// that inference honest.
|
||||||
|
expect(typeof core.useBackupModule).toBe("function");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deliberately last, and deliberately destructive: it takes the
|
||||||
|
// fallback, which replaces the loaded module for the rest of this
|
||||||
|
// file. Jest gives each test file its own module registry, so nothing
|
||||||
|
// outside sees it.
|
||||||
|
//
|
||||||
|
// Without this, the check above would be a claim about libsodium's
|
||||||
|
// internals with nothing holding it to account: if a future version
|
||||||
|
// kept useBackupModule on the fallback module too, the marker would
|
||||||
|
// quietly become true in both backends and the test would pass while
|
||||||
|
// measuring nothing. Forcing the fallback and watching the marker
|
||||||
|
// disappear is what makes its presence mean something.
|
||||||
|
test("the fallback marker distinguishes the two backends", async () => {
|
||||||
|
const core = require(SODIUM_CORE);
|
||||||
|
await require("libsodium-wrappers-sumo").ready;
|
||||||
|
expect(typeof core.useBackupModule).toBe("function");
|
||||||
|
await core.useBackupModule();
|
||||||
|
expect(typeof core.useBackupModule).toBe("undefined");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user