mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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>
67 lines
2.9 KiB
JavaScript
67 lines
2.9 KiB
JavaScript
/**
|
|
* Pure-logic tests for key-import input classification (nsec vs NIP-49
|
|
* ncryptsec) and submit gating.
|
|
*/
|
|
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { nsecEncode } from "nostr-tools/nip19";
|
|
import { generateSecretKey } from "nostr-tools/pure";
|
|
import {
|
|
classifyKeyImportInput,
|
|
isPlausibleNcryptsec,
|
|
keyImportSubmitEnabled,
|
|
} from "./keyImportInput.ts";
|
|
|
|
// NIP-49 spec vector — structurally valid encrypted backup.
|
|
const NCRYPTSEC =
|
|
"ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p";
|
|
|
|
const VALID_NSEC = nsecEncode(generateSecretKey());
|
|
|
|
test("classify_by_hrp_with_whitespace_tolerance", () => {
|
|
assert.equal(classifyKeyImportInput(` ${NCRYPTSEC}\n`), "ncryptsec");
|
|
assert.equal(classifyKeyImportInput(VALID_NSEC), "nsec");
|
|
assert.equal(classifyKeyImportInput("npub1whatever"), "unknown");
|
|
assert.equal(classifyKeyImportInput(""), "unknown");
|
|
// nsec must not be shadowed by the longer HRP check.
|
|
assert.equal(classifyKeyImportInput("nsec1"), "nsec");
|
|
});
|
|
|
|
test("uppercase_bech32_encoding_classifies_and_gates_like_lowercase", () => {
|
|
// Bech32 permits an all-uppercase encoding; it must route to the
|
|
// encrypted path (matching Rust) and be submit-plausible.
|
|
const upper = NCRYPTSEC.toUpperCase();
|
|
assert.equal(classifyKeyImportInput(upper), "ncryptsec");
|
|
assert.equal(isPlausibleNcryptsec(upper), true);
|
|
assert.equal(keyImportSubmitEnabled(upper, ""), false);
|
|
assert.equal(keyImportSubmitEnabled(upper, "hunter2hunter2"), true);
|
|
// Mixed case: routed encrypted (Rust reports the accurate error) but
|
|
// never plausible/submittable — mixed-case bech32 cannot decode.
|
|
const mixed = `N${NCRYPTSEC.slice(1)}`;
|
|
assert.equal(classifyKeyImportInput(mixed), "ncryptsec");
|
|
assert.equal(isPlausibleNcryptsec(mixed), false);
|
|
assert.equal(keyImportSubmitEnabled(mixed, "hunter2hunter2"), false);
|
|
});
|
|
|
|
test("plausible_ncryptsec_requires_bech32_charset", () => {
|
|
assert.equal(isPlausibleNcryptsec(NCRYPTSEC), true);
|
|
// '1' and 'b' / 'i' / 'o' are not in the bech32 charset.
|
|
assert.equal(isPlausibleNcryptsec("ncryptsec1bio"), false);
|
|
assert.equal(isPlausibleNcryptsec("ncryptsec1"), false);
|
|
assert.equal(isPlausibleNcryptsec("ncryptsec1 with spaces"), false);
|
|
});
|
|
|
|
test("submit_gating_nsec_path_unchanged", () => {
|
|
assert.equal(keyImportSubmitEnabled(VALID_NSEC, ""), true);
|
|
assert.equal(keyImportSubmitEnabled("nsec1garbage", ""), false);
|
|
assert.equal(keyImportSubmitEnabled("", ""), false);
|
|
});
|
|
|
|
test("submit_gating_ncryptsec_requires_passphrase", () => {
|
|
assert.equal(keyImportSubmitEnabled(NCRYPTSEC, ""), false);
|
|
assert.equal(keyImportSubmitEnabled(NCRYPTSEC, "hunter2hunter2"), true);
|
|
// Structurally implausible blob never submits, passphrase or not.
|
|
assert.equal(keyImportSubmitEnabled("ncryptsec1bio", "hunter2"), false);
|
|
});
|