mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
- Built-in Tor (kmp-tor, bundled for Linux/macOS/Windows) or an external SOCKS proxy (system tor 9050 / Tor Browser 9150), selectable under Settings -> Tor setup, mirroring the Android TorMode. Relay dials fail closed while built-in Tor is starting; switching modes or Tor coming up redials every relay through the new route. - Local relays (localhost, RFC 1918, link-local, CGNAT/Tailscale, IPv6 ULA, .local/.lan/.home.arpa) always bypass Tor, get ws:// by default, are tagged in the relay list, and stay connected when the internet drops. Host-based detection replaces the substring match that flagged public hosts like relay10.example.com as private. - Port of the Android cleartext ws:// warning for public relays. - Strings translated in all 14 languages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
197 lines
10 KiB
Markdown
197 lines
10 KiB
Markdown
# Amber Desktop (Windows, macOS, Linux)
|
||
|
||
A Compose for Desktop port of Amber that turns your computer into a NIP-46
|
||
remote signer ("bunker"). It shares the same Nostr stack as the Android app
|
||
(the [Quartz](https://github.com/vitorpamplona/amethyst) library, published
|
||
for the JVM) and mirrors the mobile UI and permission model.
|
||
|
||
## Features
|
||
|
||
- Multiple accounts: create a new key (NIP-06 seed words) or import an
|
||
`nsec`, `ncryptsec` (NIP-49), raw hex key, or mnemonic
|
||
- NIP-46 signing over relays: `connect`, `sign_event`, `get_public_key`,
|
||
`ping`, `nip04_encrypt/decrypt`, `nip44_encrypt/decrypt`,
|
||
`nip44v3_encrypt/decrypt`, `decrypt_zap_event`, `sign_psbt`,
|
||
`switch_relays`, `logout`
|
||
- Connect applications with a `nostrconnect://` URI or by generating a
|
||
`bunker://` URI (with QR code) — each connection gets its own local key.
|
||
Clicking a `nostrconnect://` link opens Amber (Linux: registered per user
|
||
via `xdg-mime`; macOS: declared in the app bundle's Info.plist)
|
||
- The same permission model as mobile: auto-accept / auto-reject rules per
|
||
request type and event kind, time-bound grants (5 minutes … always), and
|
||
per-application sign policies (basic / manual / sign everything)
|
||
- Per-application activity history and relay logs
|
||
- Default bunker relays management
|
||
- Tor (Settings → Tor setup), like the Android app: a built-in Tor daemon
|
||
(kmp-tor, bundled for every OS) or an external SOCKS proxy such as the
|
||
system tor service (port 9050) or Tor Browser (9150). Relay connections
|
||
fail closed while built-in Tor is still starting, and switching modes
|
||
redials every relay through the new route
|
||
- Local relays: localhost, LAN/VPN addresses (RFC 1918, link-local, CGNAT /
|
||
Tailscale, IPv6 ULA) and `.local` / `.lan` / `.home.arpa` names get plain
|
||
`ws://` by default, always bypass Tor, and stay connected when the internet
|
||
drops. An explicit `ws://` relay on the public internet shows a cleartext
|
||
warning
|
||
- System tray: closing the window minimizes Amber to the tray so it keeps
|
||
answering requests (with Open / Lock now / Quit menu), and new approval
|
||
requests raise a system notification and bring the window back — both
|
||
configurable under Settings → Desktop. The tray uses the freedesktop
|
||
StatusNotifierItem / AppIndicator protocol on Linux (via the dorkbox
|
||
SystemTray library), so the icon shows on Wayland compositors such as
|
||
Hyprland/Sway (through waybar's tray module) and on GNOME/KDE, not just
|
||
X11. It needs an SNI host (e.g. waybar's `tray` module — note it may sit
|
||
inside a `group`/drawer that you expand to reveal the icon) and the
|
||
`libayatana-appindicator` runtime library; Amber automatically bridges the
|
||
Ayatana library to the legacy `libappindicator3` names dorkbox looks for,
|
||
so no compat symlink is required. When no SNI host is on the session bus
|
||
(or tray init takes too long), Amber skips the tray and logs why instead of
|
||
blocking startup. `AMBER_TRAY_TYPE=Gtk|AppIndicator|AutoDetect`
|
||
forces the backend and `AMBER_DISABLE_TRAY=1` skips the tray entirely.
|
||
- Notifications go through the OS-native channel: the freedesktop
|
||
notification daemon (mako, dunst, swaync, GNOME Shell, …) via `notify-send`
|
||
or `gdbus` on Linux — so they work on Hyprland/Wayland — and the AWT tray
|
||
notification on Windows and macOS (on macOS it is posted as Amber itself;
|
||
allow it when macOS asks on first launch, or later under System Settings →
|
||
Notifications → Amber)
|
||
- Mandatory passphrase lock (see Key storage below)
|
||
- Optional start-on-boot (Settings → Desktop), always starting locked —
|
||
passphrase required before anything signs. Only offered for installed
|
||
builds (not `:desktop:run`):
|
||
- Windows: a per-user `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`
|
||
entry (no admin rights; also listed under Task Manager → Startup apps)
|
||
- macOS: a per-user LaunchAgent (`~/Library/LaunchAgents/com.greenart7c3.nostrsigner.plist`)
|
||
that runs Amber at login (listed under System Settings → General → Login
|
||
Items & Extensions)
|
||
- Linux: installs and enables a hardened systemd **user** service that
|
||
starts Amber with the desktop session. No `MemoryDenyWriteExecute` (the
|
||
JVM's JIT cannot run under it); the unit still gets
|
||
`ProtectSystem=strict`, seccomp, `NoNewPrivileges` and friends
|
||
- Windows installer (MSI/EXE) adds a Start Menu entry and a desktop shortcut
|
||
- The window opens fitted to the screen's usable area (never under the
|
||
taskbar) and remembers its size and maximized state
|
||
- Pending requests expire after 10 minutes (or at the request's NIP-40
|
||
`expiration`, if sooner), since NIP-46 clients stop waiting long before
|
||
- Native desktop layout: sidebar navigation with an account switcher, dense
|
||
list views, and keyboard shortcuts
|
||
- Light/dark theme using the Amber palette
|
||
- Fully localized UI in 14 languages, switchable live under Settings →
|
||
Language. The Android `strings.xml` translations (and event-kind
|
||
descriptions) are bundled verbatim under `resources/i18n/strings_<lang>.xml`
|
||
and loaded by `core/Strings.kt`; desktop-only strings live in
|
||
`strings_en.xml` (prefixed `d_`) and fall back to English in other locales
|
||
|
||
## Keyboard shortcuts
|
||
|
||
Ctrl on Windows/Linux, ⌘ on macOS:
|
||
|
||
| Shortcut | Action |
|
||
|----------|--------|
|
||
| Ctrl/⌘ + 1–4 | Switch between Incoming requests / Applications / Relays / Settings |
|
||
| ↑ / ↓ | Select a pending request (Incoming requests) |
|
||
| ← / → | Cycle the selected request's "Remember" duration |
|
||
| Ctrl/⌘ + Enter | Approve the selected request with the chosen duration |
|
||
| Ctrl/⌘ + Shift + Enter | Reject the selected request |
|
||
| Escape | Leave the application detail view |
|
||
| Ctrl/⌘ + L | Lock |
|
||
| Ctrl/⌘ + M | Minimize to tray (keep running in the background) |
|
||
| Ctrl/⌘ + W | Same as Ctrl/⌘ + M |
|
||
| Ctrl/⌘ + Q | Quit |
|
||
|
||
Not included: NIP-55 (`nostrsigner:` intents and the content provider) —
|
||
that is Android IPC and does not exist on desktop. Web apps and other
|
||
clients connect through NIP-46 instead.
|
||
|
||
## Key storage
|
||
|
||
Private keys are encrypted at rest with AES-256-GCM. The AES key is held in
|
||
a Java KeyStore (PKCS12) file under the application data directory:
|
||
|
||
- Windows: `%APPDATA%\Amber`
|
||
- macOS: `~/Library/Application Support/Amber`
|
||
- Linux: `$XDG_DATA_HOME/amber` (or `~/.local/share/amber`)
|
||
|
||
The keystore password is kept in the operating system's credential store:
|
||
|
||
- macOS: Keychain
|
||
- Windows: Credential Manager
|
||
- Linux: the freedesktop Secret Service (GNOME Keyring / KWallet over D-Bus)
|
||
|
||
so copying the data directory (or a backup of it) is not enough to unlock
|
||
the keys. On systems without a secret daemon (headless Linux, minimal
|
||
window managers) the password falls back to an owner-only file next to the
|
||
keystore, and is migrated into the credential store automatically the
|
||
first time one becomes available. The Settings screen shows which backend
|
||
is in use.
|
||
|
||
Note the trust model: any process running as your OS user can request the
|
||
secret from the credential store, so this protects against offline attacks
|
||
(disk theft, leaked backups, copied data directories) rather than against
|
||
malware running in your session. Use full-disk encryption and OS login
|
||
protection too. If you move the data directory to another machine, also
|
||
transfer the `com.greenart7c3.nostrsigner` entry from the credential store
|
||
(or keep the legacy `keystore.pass` file).
|
||
|
||
### Passphrase lock (mandatory)
|
||
|
||
Amber requires a passphrase: on first run a setup screen asks for one before
|
||
anything else can be used, and installs that already have only the OS
|
||
credential store are migrated to it on the next launch. The AES master key
|
||
is then stored only wrapped (AES-256-GCM) under a key derived from your
|
||
passphrase with **Argon2id**, in `master.key.enc`; the plain keystore and its
|
||
credential-store/file password are deleted. The passphrase is never written
|
||
anywhere. You can change it under **Settings → Security**, but there is no
|
||
way to remove it and go back to unprotected storage.
|
||
|
||
With the lock on:
|
||
|
||
- Copying the data directory (or the credential store) is useless — without
|
||
the passphrase there is no way to decrypt the keys.
|
||
- The per-account **database** (connected applications, permission grants,
|
||
request history, relay logs) is also encrypted at rest with the master
|
||
key — AES-256-GCM, with an `AMBERENC1:` header — so the metadata about
|
||
which apps you sign for stays private too. Enabling the passphrase
|
||
re-encrypts existing data immediately.
|
||
(`settings.json` and `accounts.json` stay plaintext, but the private keys
|
||
inside `accounts.json` are always encrypted with the master key.)
|
||
- Amber asks for the passphrase at startup and auto-locks after an idle
|
||
timeout — 1 hour by default, selectable (5 min / 15 min / 1 hour / never)
|
||
under **Settings → Security** — or immediately via **Lock now**. Locking
|
||
evicts all key material from memory and disconnects the relays, so no
|
||
request can be signed until you unlock again.
|
||
- Logging out of an account (which deletes its key from this device) asks
|
||
for the passphrase first.
|
||
|
||
Residual risk it cannot remove: while unlocked, the keys are in the
|
||
process's memory, so malware that can scrape another process's memory or
|
||
log your keystrokes in your session could still capture them — that is
|
||
inherent to any software signer on a general-purpose OS. Hardware-backed,
|
||
per-signature consent (Touch ID / Windows Hello / TPM) would be the next
|
||
step and is tracked as future work.
|
||
|
||
**If you forget the passphrase there is no recovery** — restore your keys
|
||
from their nsec or seed-word backup instead.
|
||
|
||
## Run and build
|
||
|
||
```bash
|
||
./gradlew :desktop:run # run from source
|
||
./gradlew :desktop:createDistributable # runnable app image
|
||
./gradlew :desktop:packageDeb # Linux .deb
|
||
./gradlew :desktop:packageRpm # Linux .rpm
|
||
./gradlew :desktop:packageMsi # Windows .msi (build on Windows)
|
||
./gradlew :desktop:packageExe # Windows .exe (build on Windows)
|
||
./gradlew :desktop:packageDmg # macOS .dmg (build on macOS)
|
||
./gradlew :desktop:packageDistributionForCurrentOs # whatever fits the host
|
||
```
|
||
|
||
jpackage can only produce installers for the OS it runs on, so release
|
||
builds are made per-platform. Linux packaging needs `fakeroot` (deb) or
|
||
`rpm-build` (rpm) installed.
|
||
|
||
## Tests
|
||
|
||
```bash
|
||
./gradlew :desktop:test # unit tests
|
||
AMBER_E2E=1 ./gradlew :desktop:test # + a NIP-46 round-trip over a public relay
|
||
```
|