Files
buzz/desktop/src/shared/ui/card-texture.css
Tyler 468647a51f feat(desktop): locally stored NIP-49 encrypted key backup (#2937)
## Summary

Adds a locally stored **NIP-49 encrypted key backup** (`ncryptsec`) to
the desktop app, per the plan reviewed in buzz-development (Rev 3,
approved 9/10 by Wren; implementation also reviewed and approved 9/10).

**Two-artifact design — canonical bytes originate entirely in Rust:**
- `create_ncryptsec_backup` runs under the `identity_mutation` lock:
encrypt → decrypt-verify against the live pubkey → atomic `0o600` write
to `{app_data_dir}/identity.ncryptsec` → reread/byte-compare → return
the exact persisted bytes. The frontend never re-derives or re-encrypts.
- `save_ncryptsec_copy` writes a portable copy via the save dialog
(parse-gated, secret-file semantics) and never mutates canonical state.
- `generate_backup_passphrase`: 6 words from the EFF short wordlist via
`OsRng` (custom passphrases min 12 chars).
- Import accepts `ncryptsec1` with optional password; the raw-`nsec`
path is untouched. Different-pubkey import and sign-out wipe the
app-managed backup (post-commit, best-effort — a failed import can never
destroy the still-live identity's backup; regression-tested).

**Never-relay guarantee (egress guard + tripwires):**
- `egress_guard.rs` fail-closed at all 8 `/events` submission boundaries
(relay submit funnel, 3× `relay.rs`, huddle STT, both engram submitters,
native WS choke point), rejecting `ncryptsec1`/`NCRYPTSEC1` in text and
binary frames. Scope is deliberately ncryptsec-only: pairing
intentionally carries raw nsec inside its encrypted session.
- Site-granular `/events` inventory tripwire: per-file (`/events` count,
guard-call count) pairs; unlisted files expect zero. Mutation-style
tests prove a ninth site in an existing file, a removed guard, and a new
unlisted file all fail the scan.
- ncryptsec source-allowlist scans in **both** trees (Rust + TS).

**Frontend:** onboarding `BackupStep` is encrypted-by-default — the
default path never invokes `get_nsec` (e2e asserts the command log).
Raw-nsec export stays behind an explicit click with prior semantics.
Shared `EncryptedBackupCreator` powers onboarding + a new settings row;
the import form auto-switches to encrypted mode on `ncryptsec1` paste
(case-insensitive HRP).

**Open product call for @tlongwell-block:** onboarding default is
*encrypted* in this PR; flipping to raw-default is a small change either
way (documented in the plan).

Review history: plan Rev 3 and the implementation were both iterated
with Wren to 9/10 (two blockers from round 1 — import ordering,
inventory granularity — plus an uppercase-bech32 hardening gap, all
fixed in `dde37183e`). Thread: buzz-development.

### Related issue

Follow-up to the direction explored in #385 (NIP-PB, closed) — this
ships local NIP-49 (the standard) instead of a new NIP. No open
duplicate found.

### Testing

All at exactly `dde37183e` (same shell, HEAD verified):

- `cargo test` — 1680 passed / 0 failed / 14 ignored (includes a
deliberate ~70s log_n-18 NIP-49 round trip, spec vector, wrong-password,
NFKC, uppercase-vector decrypt, injection test per egress boundary,
inventory mutation tests, import-ordering regression tests)
- `cargo clippy --all-targets -- -D warnings` — clean; `cargo fmt
--check` — clean
- `pnpm typecheck` — clean; JS unit suite 3529/3529; biome (repo-pinned
2.4.16) clean
- Playwright `onboarding-backup` / `onboarding` /
`onboarding-agent-defaults` / `profile-nsec-reveal` — 86 passed, 1 known
avatar-reservation flake (passed on rerun; untouched by this diff).
`passThroughBackupStep` now exercises the encrypted default, so every
downstream onboarding spec covers the new path.
- Note: browser e2e fakes the crypto via the mock bridge (fixed
spec-vector blob); decryption correctness is proven in the Rust tests.

## Latest onboarding integration

The current head adds an additive `IdentityInfo.storage` field
(`ephemeral`, `system-keyring`, `local-file`, or `environment`) so
onboarding can accurately explain where the active identity is
protected. It surfaces storage metadata only—never key material—and
leaves the existing lost/keyring-locked recovery behavior intact.

---------

Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
2026-07-30 20:58:03 -04:00

77 lines
3.2 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Baked nine-slice texture generated by
* desktop/scripts/texture-card/generate-card-texture.mjs.
*
* The source is 2×: 416 asset pixels = 208 CSS pixels. The image includes a
* 96 CSS-pixel transparent outer powder band. `border-image` keeps corners
* fixed and repeats edge texture while the white center fills any card size.
*/
.buzz-card-textured {
--buzz-card-textured-source: url("./assets/card-texture.png");
--buzz-card-textured-slice: 416;
--buzz-card-textured-width: 208px;
--buzz-card-textured-outset: 96px;
--buzz-card-textured-min-size: 224px;
/*
* The asset has three zones, from outside in:
* 1. transparent powder bleed (outside the layout box, via outset)
* 2. feathered fade (inside the layout edge, white is NOT fully opaque)
* 3. solid white center (safe for content)
*
* `--buzz-card-textured-safe-inset` is the distance from the layout edge
* to zone 3. `Card variant="textured"` applies it as default padding so
* content always lands on solid fill. Features may add more padding,
* never less — and must never layer an opaque surface on top of the
* texture; the baked white center IS the surface.
*/
--buzz-card-textured-safe-inset: 5rem;
border: 0 solid transparent;
/*
* Each border-image band is 208px wide but extends 96px outside the layout
* box (outset), leaving 112px per edge inside it. Below 224px (2 × 112px)
* on either axis the opposing bands collapse into each other: the solid
* white center region disappears and the bands meet on a semi-transparent
* row of the fade, which shows the page background through as a thin seam
* line. Keep 224px as the default floor. Compact decorative cards may opt
* into a smaller height with `--buzz-card-textured-min-height` when the
* center seam is acceptable for that deliberately compressed surface.
*/
min-height: var(
--buzz-card-textured-min-height,
var(--buzz-card-textured-min-size)
);
min-width: var(
--buzz-card-textured-min-width,
var(--buzz-card-textured-min-size)
);
border-image-source: var(--buzz-card-textured-source);
border-image-slice: var(--buzz-card-textured-slice) fill;
border-image-width: var(--buzz-card-textured-width);
border-image-outset: var(--buzz-card-textured-outset);
border-image-repeat: repeat;
}
/* Dark backup surfaces use a separately baked fill so their content stays
* bright; applying `filter: brightness()` to the element would dim children. */
.buzz-card-textured-dark {
--buzz-card-textured-source: url("./assets/card-texture-dark.png");
}
/*
* Popovers, confirmations, and compact callouts need the same powder edge
* without the full card's 96px bleed or 224px floor. This independently baked
* nine-slice keeps the edge legible at overlay scale.
*/
.buzz-card-textured-compact {
--buzz-card-textured-source: url("./assets/card-texture-compact.png");
--buzz-card-textured-slice: 136;
--buzz-card-textured-width: 68px;
--buzz-card-textured-outset: 24px;
--buzz-card-textured-safe-inset: 1.75rem;
--buzz-card-textured-min-size: 136px;
}
.buzz-card-textured-dark.buzz-card-textured-compact {
--buzz-card-textured-source: url("./assets/card-texture-dark-compact.png");
}