- Your unique identity key has been created
+ Account created!
{mode === "encrypted"
- ? "Protect it with a passphrase and keep an encrypted backup in case you ever need to restore your account."
+ ? "Buzz keeps your identity in the system keychain. Save a portable, password-protected Keycase in case you need to restore it elsewhere."
: "This key is stored in your system keychain, but save it some place safe in case you ever need to restore your account."}
- Saved to {savedPath}
+ Keycase saved to {savedPath}
) : null}
@@ -150,8 +164,8 @@ export function EncryptedBackupCreator({
{saveError}
) : null}
- This backup can only be unlocked with your passphrase. Without the
- passphrase it cannot be recovered — not even by Buzz.
+ Keep this Keycase private. You need both the file and its password to
+ restore your identity. Buzz cannot reset the password.
);
@@ -180,13 +194,13 @@ export function EncryptedBackupCreator({
>
- Could not generate a passphrase: {state.generateError}
+ Could not generate a Keycase password: {state.generateError}
) : (
- Generating a passphrase…
+ Generating a password…
)}
@@ -199,7 +213,7 @@ export function EncryptedBackupCreator({
variant="outline"
>
- New passphrase
+ New generated password
- Write this passphrase down. It protects your backup and cannot be
- recovered if lost.
+ Save this generated passphrase as your Keycase password. Store it
+ separately from the private Keycase file.
) : (
@@ -268,11 +282,11 @@ export function EncryptedBackupCreator({
type="button"
variant="ghost"
>
- Use a generated passphrase
+ Use a generated password
- Your passphrase protects the backup and cannot be recovered if lost.
+ Your password protects the Keycase. Buzz cannot reset it if lost.
)}
@@ -297,10 +311,10 @@ export function EncryptedBackupCreator({
{state.isCreating ? (
<>
- Encrypting… this takes a couple of seconds
+ Creating Keycase… this takes a couple of seconds
>
) : (
- "Create encrypted backup"
+ "Create Keycase"
)}
diff --git a/desktop/src/features/onboarding/ui/NostrKeyImportForm.tsx b/desktop/src/features/onboarding/ui/NostrKeyImportForm.tsx
index dfa68e7d5..90eb1f368 100644
--- a/desktop/src/features/onboarding/ui/NostrKeyImportForm.tsx
+++ b/desktop/src/features/onboarding/ui/NostrKeyImportForm.tsx
@@ -96,7 +96,7 @@ export function NostrKeyImportForm({
if (file.size > NOSTR_KEY_FILE_MAX_BYTES) {
setImportError(
- "That file is too large to be a key. Choose a .key or .ncryptsec backup file, or paste your key.",
+ "That file is too large to be a Keycase or private key. Choose another file.",
);
return;
}
@@ -126,7 +126,7 @@ export function NostrKeyImportForm({
if (!isValid) {
setImportError(
isEncryptedInput
- ? "Enter the passphrase for this encrypted backup."
+ ? "Enter the password for this Keycase."
: "That doesn't look like a valid nsec. Paste an nsec1 key.",
);
return;
@@ -244,7 +244,7 @@ export function NostrKeyImportForm({
{/* Hidden file input shared by both variants: the default drop zone and
- the spotlight "Import from a file" button both open it. Accepts the
+ the spotlight "Use a Keycase" button both open it. Accepts the
.ncryptsec archives our own save flow emits alongside raw .key files. */}
- Import from a file
+ Use a Keycase
) : (
@@ -358,7 +358,7 @@ export function NostrKeyImportForm({
className="text-sm font-medium text-foreground"
htmlFor="nostr-import-passphrase"
>
- Backup passphrase
+ Keycase password
+
+ Your Keycase and password stay on this device.
+
) : null}
@@ -393,7 +396,7 @@ export function NostrKeyImportForm({
data-testid="nostr-import-encrypted-badge"
>
- Encrypted key backup — enter its passphrase to import
+ Keycase · Private — enter its password to restore
) : previewNpub ? (
variant === "spotlight" ? (
diff --git a/desktop/src/features/onboarding/ui/NsecMaskedDisplay.tsx b/desktop/src/features/onboarding/ui/NsecMaskedDisplay.tsx
index df4536ef1..1dda9b5e2 100644
--- a/desktop/src/features/onboarding/ui/NsecMaskedDisplay.tsx
+++ b/desktop/src/features/onboarding/ui/NsecMaskedDisplay.tsx
@@ -9,8 +9,8 @@ type NsecMaskedDisplayProps = {
variant?: "boxed" | "bare";
/**
* What kind of secret is displayed. Drives labels, aria and testids:
- * a raw private key ("nsec", default) can impersonate its holder; an
- * encrypted backup ("ncryptsec") is only as sensitive as its passphrase.
+ * a raw private key ("nsec", default) can impersonate its holder; a
+ * Keycase ("ncryptsec") is only as sensitive as its passphrase.
*/
kind?: "nsec" | "ncryptsec";
/**
@@ -26,7 +26,7 @@ const KIND_LABELS = {
testIdPrefix: "nsec",
},
ncryptsec: {
- noun: "encrypted backup",
+ noun: "Keycase",
testIdPrefix: "ncryptsec",
},
} as const;
diff --git a/desktop/src/features/onboarding/ui/onboardingFlowSteps.test.mjs b/desktop/src/features/onboarding/ui/onboardingFlowSteps.test.mjs
index 720741eff..c28be1386 100644
--- a/desktop/src/features/onboarding/ui/onboardingFlowSteps.test.mjs
+++ b/desktop/src/features/onboarding/ui/onboardingFlowSteps.test.mjs
@@ -53,74 +53,11 @@ test("currentStep_falls_back_to_1_for_pages_outside_the_step_list", () => {
});
// ---------------------------------------------------------------------------
-// BackupStep gating: backupNextDisabled() pure helper
+// BackupStep gating: saving a Keycase is recommended, not required
// ---------------------------------------------------------------------------
-test("backup_next_disabled_in_encrypted_mode_until_backup_exists", () => {
- // Encrypted (default) mode: the user must create a backup — or explicitly
- // switch to the raw key — before Next unlocks. Loading/error state belongs
- // to the raw path and must not leak into the encrypted gate.
- assert.equal(
- backupNextDisabled({
- mode: "encrypted",
- hasBackup: false,
- isLoading: false,
- loadError: null,
- }),
- true,
- );
-});
-
-test("backup_next_enabled_in_encrypted_mode_once_backup_created", () => {
- assert.equal(
- backupNextDisabled({
- mode: "encrypted",
- hasBackup: true,
- isLoading: false,
- loadError: null,
- }),
- false,
- );
-});
-
-test("backup_next_disabled_while_loading_raw_key", () => {
- // During a slow keychain read, Next must be blocked — user cannot race past
- // the key display before it is shown.
- assert.equal(
- backupNextDisabled({
- mode: "raw",
- hasBackup: false,
- isLoading: true,
- loadError: null,
- }),
- true,
- );
-});
-
-test("backup_next_disabled_on_raw_load_error", () => {
- // Error state: only the explicit "Skip for now" ghost advances; Next blocked.
- assert.equal(
- backupNextDisabled({
- mode: "raw",
- hasBackup: false,
- isLoading: false,
- loadError: "IPC error",
- }),
- true,
- );
-});
-
-test("backup_next_enabled_after_clean_raw_load", () => {
- // Key shown (or backend cleanly returned none) — user may proceed.
- assert.equal(
- backupNextDisabled({
- mode: "raw",
- hasBackup: false,
- isLoading: false,
- loadError: null,
- }),
- false,
- );
+test("backup_next_is_always_enabled", () => {
+ assert.equal(backupNextDisabled(), false);
});
// ---------------------------------------------------------------------------
diff --git a/desktop/src/features/settings/ui/ProfileSettingsCard.tsx b/desktop/src/features/settings/ui/ProfileSettingsCard.tsx
index 5326243ff..bec4d2e4a 100644
--- a/desktop/src/features/settings/ui/ProfileSettingsCard.tsx
+++ b/desktop/src/features/settings/ui/ProfileSettingsCard.tsx
@@ -178,7 +178,7 @@ function NsecRevealRow() {
}
/**
- * Collapsible row for creating an encrypted NIP-49 backup on demand. The raw
+ * Collapsible row for saving a portable Keycase on demand. The raw
* private key never reaches this flow — the passphrase goes to Rust, which
* returns the persisted `ncryptsec1…` blob.
*/
@@ -189,9 +189,10 @@ function EncryptedBackupRow() {
-
Password Backup
+
Keycase
- Protect your key with a password and save a recoverable backup.
+ Save a portable, password-protected copy of your identity. Keep it
+ private.
{isOpen ? (
+
+ Buzz cannot reset the password. Saving a new Keycase does not
+ invalidate copies you saved before.
+
) : null}
diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts
index 741e57573..c376f6266 100644
--- a/desktop/src/testing/e2eBridge.ts
+++ b/desktop/src/testing/e2eBridge.ts
@@ -9608,7 +9608,7 @@ export function maybeInstallE2eTauriMocks() {
case "save_ncryptsec_copy": {
const blob = (payload as { ncryptsec?: string } | null)?.ncryptsec;
if (!blob?.startsWith("ncryptsec1")) {
- throw new Error("Not a valid encrypted key backup.");
+ throw new Error("Not a valid Keycase.");
}
// Production opens a native save dialog; the harness pretends the
// user picked a path.
@@ -9644,7 +9644,7 @@ export function maybeInstallE2eTauriMocks() {
input.trim() !== MOCK_NCRYPTSEC ||
request?.password !== MOCK_BACKUP_PASSPHRASE
) {
- throw new Error("Wrong passphrase or corrupted backup.");
+ throw new Error("Wrong Keycase password or damaged Keycase.");
}
mockIdentityLostCleared = true;
mockIdentityLockedCleared = true;
diff --git a/desktop/tests/e2e/onboarding-backup.spec.ts b/desktop/tests/e2e/onboarding-backup.spec.ts
index 167e274bf..e123e0079 100644
--- a/desktop/tests/e2e/onboarding-backup.spec.ts
+++ b/desktop/tests/e2e/onboarding-backup.spec.ts
@@ -21,31 +21,31 @@ test("backup step appears on fresh-key path after profile submit", async ({
await expect(page.getByTestId("onboarding-page-backup")).toBeVisible();
await expect(
page.getByRole("heading", {
- name: "Your unique identity key has been created",
+ name: "Account created!",
}),
).toBeVisible();
});
// ---------------------------------------------------------------------------
-// Encrypted-by-default path (plan D3): passphrase → create → ncryptsec shown.
+// Keycase path: password → create locally → native save → saved confirmation.
// The raw key must never be fetched on this path.
// ---------------------------------------------------------------------------
-test("encrypted backup happy path: generated passphrase, create, save copy, Next", async ({
+test("Keycase happy path: generated password, create, native save, Next", async ({
page,
}) => {
await enterMachineBackup(page);
- // Default mode: generated passphrase shown, Next locked until backup exists.
+ // Default mode: generated password shown, but backup remains optional.
await expect(page.getByTestId("backup-passphrase-generated")).toBeVisible();
- await expect(page.getByTestId("onboarding-next")).toBeDisabled();
+ await expect(page.getByTestId("onboarding-next")).toBeEnabled();
await waitForAnimations(page);
await page.screenshot({ path: `${SHOTS}/02-backup-step-passphrase.png` });
await page.getByTestId("encrypted-backup-create").click();
- // The persisted blob is displayed masked; copy + save-a-copy available.
+ // The locally created blob stays masked; the portable save action is explicit.
const blob = page.getByTestId("ncryptsec-value");
await expect(blob).toBeVisible();
await expect(blob).toHaveCSS("filter", /blur/);
@@ -55,7 +55,6 @@ test("encrypted backup happy path: generated passphrase, create, save copy, Next
await waitForAnimations(page);
await page.screenshot({ path: `${SHOTS}/03-backup-step-encrypted.png` });
- await page.getByTestId("encrypted-backup-save-copy").click();
await expect(page.getByTestId("encrypted-backup-saved-path")).toContainText(
"identity.ncryptsec",
);
@@ -98,7 +97,7 @@ test("custom passphrase requires 12 characters and confirmation", async ({
});
// ---------------------------------------------------------------------------
-// Raw-key path: preserved behind an explicit "Show raw key instead" click.
+// Raw-key path: preserved behind one explicit advanced action.
// ---------------------------------------------------------------------------
test("raw key path is one explicit click away and shows the masked nsec", async ({
@@ -124,7 +123,7 @@ test("raw key path is one explicit click away and shows the masked nsec", async
await waitForAnimations(page);
await page.screenshot({ path: `${SHOTS}/04-backup-step-raw-revealed.png` });
- // Raw mode keeps the previous gating: key shown → Next enabled.
+ // Next remains enabled on the advanced raw-key path.
await expect(page.getByTestId("onboarding-next")).toBeEnabled();
await page.getByTestId("onboarding-next").click();
await expect(page.getByTestId("onboarding-page-2")).toBeVisible();
@@ -168,8 +167,8 @@ test("raw path shows error banner and retry button when get_nsec fails", async (
await expect(page.getByTestId("backup-load-error")).toBeVisible();
await expect(page.getByTestId("backup-retry")).toBeVisible();
- // Next is blocked on error; Skip for now ghost is shown instead.
- await expect(page.getByTestId("onboarding-next")).toBeDisabled();
+ // Keychain failure does not trap the user; both Next and explicit skip work.
+ await expect(page.getByTestId("onboarding-next")).toBeEnabled();
await expect(page.getByTestId("backup-skip")).toBeVisible();
// Skip for now still advances to machine setup.
diff --git a/desktop/tests/e2e/onboarding.spec.ts b/desktop/tests/e2e/onboarding.spec.ts
index d82765496..0cf08f9be 100644
--- a/desktop/tests/e2e/onboarding.spec.ts
+++ b/desktop/tests/e2e/onboarding.spec.ts
@@ -645,7 +645,7 @@ test("first-launch encrypted backup import asks for a passphrase and continues",
await page.getByTestId("nostr-import-passphrase").fill("wrong passphrase");
await page.getByTestId("nostr-import-submit").click();
await expect(page.getByTestId("nostr-import-feedback")).toContainText(
- /wrong passphrase/i,
+ /wrong Keycase password/i,
);
await page