README.md raw

Smesh Signer

NIP-07 browser extension for Nostr identity management and event signing. Manages multiple identities in an Argon2id-encrypted vault. Exposes window.nostr to web applications for signing without revealing private keys.

What It Does

Build From Source

Prerequisites

Bun (or npm/node 20+).

Local Build

cd next/signer
bun install
bun run build:firefox    # dist/firefox/  (unpacked extension)
bun run xpi              # dist/smesh_signer-*.zip

Docker Build

No bun/node required on host:

cd next/signer
docker build -t smesh-signer-build .
mkdir -p out
docker run --rm -v $(pwd)/out:/out smesh-signer-build

Output lands in out/firefox/ (unpacked) and out/smesh_signer-*.zip (packaged).

Install

Firefox

From XPI (permanent):

  1. about:addons > gear icon > "Install Add-on From File..."
  2. Select smesh_signer-*.zip

From source (temporary, for development):

  1. about:debugging > "This Firefox" > "Load Temporary Add-on..."
  2. Select any file inside dist/firefox/
  3. Extension persists until browser restart

Chrome / Chromium

  1. chrome://extensions > enable "Developer mode"
  2. "Load unpacked" > select dist/firefox/ directory
  3. The MV3 manifest is compatible with both browsers

Architecture

Three-layer message bridge between web pages and the encrypted vault:

Web page                    Extension
--------                    ---------
window.nostr.signEvent()
  |
  v
[Injected Script]          smesh-signer-extension.js
  | window.postMessage()     (runs in page context)
  v
[Content Script]           smesh-signer-content-script.js
  | browser.runtime.sendMessage()
  v
[Background SW]            background.js
  | vault decrypt + BIP340 sign
  v
  (response bubbles back through same chain)

Message Flow Detail

  1. Page calls window.nostr.signEvent(event)
  2. Injected script wraps call as {type: "smesh-signer-request", method, params}, posts to window
  3. Content script catches window message, forwards via browser.runtime.sendMessage()
  4. Background receives, checks permission policy, performs crypto, returns result
  5. Reverse path: background -> content script -> injected script -> Promise resolves

Permission Model

Per-identity, per-host, per-method granularity:

Vault Encryption

password
  |
  v  Argon2id (256MB mem, 4 threads, 8 iterations, ~3s)
  |
  v  32-byte derived key
  |
  v  AES-256-GCM (random 12-byte IV per entry)
  |
  v  encrypted vault blob  -->  browser.storage.local

Key cached in browser.storage.session while vault is unlocked. Cleared on lock or browser close.

Crypto Proxy Integration

When smesh detects the signer extension, it delegates all crypto to the extension instead of requiring an nsec. The Marmot SW sends crypto requests through the bus:

Marmot SW  -->  Bus WS  -->  Shell SW  -->  Page  -->  window.nostr.nip44Encrypt()
                                                          |
                                                    Signer Extension
                                                          |
                                                    (vault decrypt + ECDH + encrypt)

This is the ProxyCrypto path in pkg/nostr/protocol/marmot/crypto.go. The alternative LocalCrypto path holds the key directly (bridge/bridgebot mode).

Development

Watch mode for iterative development:

bun run watch:firefox

Rebuilds on source change. Reload extension in about:debugging after each rebuild.

Debug logging: uncomment console.log inside debug() at src/smesh-signer-extension.ts:248.

Project Structure

next/signer/
  src/
    background.ts               Background SW (vault, crypto, dispatch)
    smesh-signer-extension.ts   Injected into page (window.nostr API)
    smesh-signer-content-script.ts  Content script (message bridge)
    prompt.ts                   Permission prompt popup
    options.ts                  Extension options page
    unlock.ts                   Vault unlock page
    app/                        Angular popup UI (identity management)
  public/
    manifest.json               MV3 manifest (Firefox + Chrome compatible)
    *.html                      Popup/options/prompt/unlock pages
  common/                       Shared Angular library (@common)
  custom-webpack.config.ts      Extra webpack entries (background, content script, etc.)