Files
buzz/desktop/src/features/onboarding/lib/keyImportInput.ts
npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d dde37183e3 Address implementation-review blockers: import ordering, site-granular inventory, uppercase bech32
Blocker 1: import_identity persisted-before-cleanup ordering. New
commit_imported_identity helper runs durable persistence FIRST; a failed
different-key import now leaves both the old in-memory identity and its
valid canonical identity.ncryptsec intact. Stale-backup cleanup runs after
the durable commit and is deliberately best-effort (logged, not surfaced
as a half-applied-import error). Regression tests cover the failure path
and prove cleanup cannot precede persist.

Blocker 2: the /events inventory tripwire is now site-granular. Each
inventoried file pairs an expected non-comment /events occurrence count
with an expected egress-guard call count, so an unguarded ninth site in an
already-listed file (or a deleted guard call) fails the scan. The scan
core is pure over (path, content) pairs, with mutation-style tests
demonstrating all three drift classes.

Hardening: bech32 permits an all-uppercase encoding, so the egress guard
now rejects NCRYPTSEC1... too (mixed case stays unblocked - it cannot
decode), and both import classifiers (Rust recover_keys_from_input, TS
classifyKeyImportInput/isPlausibleNcryptsec) route uppercase-valid blobs
to the encrypted path consistently.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
2026-07-25 23:45:09 -04:00

52 lines
1.9 KiB
TypeScript

/**
* Pure classification + submit gating for the key-import form, unit-testable
* without a DOM.
*
* `ncryptsec1…` is a NIP-49 encrypted backup: no npub preview is possible
* (the pubkey is inside the encrypted payload) and a passphrase is required.
* Full validation happens in Rust at decrypt time; here we only classify by
* HRP shape so the form can switch modes while the user is mid-paste.
*/
import { nsecToNpub } from "@/shared/lib/nostrUtils";
export type KeyImportKind = "nsec" | "ncryptsec" | "unknown";
/**
* Bech32 charset after the `ncryptsec1` HRP; anything else can't decode.
* Bech32 also permits an ALL-UPPERCASE encoding of the same payload, so both
* casings are plausible (mixed case is invalid and stays implausible).
*/
const NCRYPTSEC_SHAPE =
/^(?:ncryptsec1[02-9ac-hj-np-z]{10,}|NCRYPTSEC1[02-9AC-HJ-NP-Z]{10,})$/;
export function classifyKeyImportInput(input: string): KeyImportKind {
const trimmed = input.trim();
// Case-insensitive on the HRP to match the Rust classifier: an uppercase
// valid backup routes to the encrypted path (and decodes there); mixed
// case routes there too and fails in Rust with the accurate error.
if (trimmed.slice(0, 10).toLowerCase() === "ncryptsec1") return "ncryptsec";
if (trimmed.startsWith("nsec1")) return "nsec";
return "unknown";
}
/** Structurally plausible encrypted backup (real validation is in Rust). */
export function isPlausibleNcryptsec(input: string): boolean {
return NCRYPTSEC_SHAPE.test(input.trim());
}
/**
* Whether the import form's submit should be enabled.
* nsec: must derive an npub. ncryptsec: plausible blob + non-empty passphrase.
*/
export function keyImportSubmitEnabled(
input: string,
passphrase: string,
): boolean {
const kind = classifyKeyImportInput(input);
if (kind === "ncryptsec") {
return isPlausibleNcryptsec(input) && passphrase.length > 0;
}
return nsecToNpub(input) !== null;
}