From 853f6e6d67823b5a58c4d799c8c185a192e0426f Mon Sep 17 00:00:00 2001 From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> Date: Tue, 28 Jul 2026 12:46:30 -0700 Subject: [PATCH 01/21] feat(identity): add optional Keycase backup flow Co-authored-by: Taylor Ho Signed-off-by: Taylor Ho --- desktop/src-tauri/src/commands/identity.rs | 2 +- desktop/src-tauri/src/key_backup.rs | 5 +- desktop/src-tauri/src/key_backup_tests.rs | 8 +- .../src/features/onboarding/ui/BackupStep.tsx | 60 ++++---------- .../onboarding/ui/EncryptedBackupCreator.tsx | 82 +++++++++++-------- .../onboarding/ui/NostrKeyImportForm.tsx | 17 ++-- .../onboarding/ui/NsecMaskedDisplay.tsx | 6 +- .../ui/onboardingFlowSteps.test.mjs | 69 +--------------- .../settings/ui/ProfileSettingsCard.tsx | 13 ++- desktop/src/testing/e2eBridge.ts | 4 +- desktop/tests/e2e/onboarding-backup.spec.ts | 21 +++-- desktop/tests/e2e/onboarding.spec.ts | 2 +- 12 files changed, 111 insertions(+), 178 deletions(-) diff --git a/desktop/src-tauri/src/commands/identity.rs b/desktop/src-tauri/src/commands/identity.rs index bb3ef796bb..b5074f4437 100644 --- a/desktop/src-tauri/src/commands/identity.rs +++ b/desktop/src-tauri/src/commands/identity.rs @@ -278,7 +278,7 @@ pub async fn save_ncryptsec_copy( let dest = match crate::commands::export_util::pick_save_path( &app_handle, crate::key_backup::BACKUP_FILE_NAME, - "Encrypted key backup", + "Keycase", &["ncryptsec"], ) .await? diff --git a/desktop/src-tauri/src/key_backup.rs b/desktop/src-tauri/src/key_backup.rs index 6db9eaa385..c8b16c1569 100644 --- a/desktop/src-tauri/src/key_backup.rs +++ b/desktop/src-tauri/src/key_backup.rs @@ -100,7 +100,7 @@ pub fn decrypt_ncryptsec(input: &str, password: &str) -> Result { let encrypted = parse_ncryptsec(input)?; let secret_key = encrypted .decrypt(password) - .map_err(|_| "wrong passphrase or corrupted backup".to_string())?; + .map_err(|_| "wrong Keycase password or damaged Keycase".to_string())?; Ok(Keys::new(secret_key)) } @@ -119,8 +119,7 @@ pub fn recover_keys_from_input(input: &str, password: Option<&str>) -> Result("encrypted"); - const [hasBackup, setHasBackup] = React.useState(false); const [nsec, setNsec] = React.useState(null); const [isLoading, setIsLoading] = React.useState(false); const [loadError, setLoadError] = React.useState(null); @@ -103,11 +80,11 @@ export function BackupStep({ direction, onBack, onNext }: BackupStepProps) { >

- 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."}

@@ -116,10 +93,14 @@ export function BackupStep({ direction, onBack, onNext }: BackupStepProps) { {mode === "encrypted" ? (
- setHasBackup(true)} - variant="spotlight" - /> +
+

Save a Keycase

+

+ A Keycase is still private. Never publish or share it. You + need both the file and password to restore your identity. +

+
+
) : isLoading ? ( @@ -164,7 +145,7 @@ export function BackupStep({ direction, onBack, onNext }: BackupStepProps) {

)} - {mode === "encrypted" && !hasBackup ? ( + {mode === "encrypted" ? (
) : null} @@ -194,12 +175,7 @@ export function BackupStep({ direction, onBack, onNext }: BackupStepProps) { {savedPath ? (

- 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 dfa68e7d5b..90eb1f368a 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" >