feat: factory reset, backup restore, auto-identity creation

- system.factory-reset RPC: wipes user data, preserves images/node_key
- Factory Reset button in Settings with confirmation modal
- backup.restore-identity RPC: decrypts and restores DID key
- Restore from Backup panel in OnboardingIntro first screen
- Auto-create default identity with Nostr key on boot if none exist

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-15 05:18:12 +00:00
co-authored by Claude Opus 4.6
parent de8dcee155
commit d1e14c4269
9 changed files with 346 additions and 13 deletions
+64
View File
@@ -66,3 +66,67 @@ pub async fn create_encrypted_backup(
"timestamp": chrono::Utc::now().to_rfc3339(),
}))
}
/// Restore a node identity key from an encrypted backup.
/// Returns the DID and pubkey of the restored identity.
pub async fn restore_encrypted_backup(
identity_dir: &Path,
backup: &serde_json::Value,
passphrase: &str,
) -> Result<(String, String)> {
let blob_b64 = backup
.get("blob")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'blob' in backup"))?;
let blob = BASE64.decode(blob_b64).context("Invalid base64 in backup blob")?;
if blob.len() < SALT_LEN + NONCE_LEN {
anyhow::bail!("Backup blob too short");
}
let salt = &blob[..SALT_LEN];
let nonce = &blob[SALT_LEN..SALT_LEN + NONCE_LEN];
let ciphertext = &blob[SALT_LEN + NONCE_LEN..];
let argon2 = Argon2::default();
let mut key = [0u8; KEY_LEN];
argon2
.hash_password_into(passphrase.as_bytes(), salt, &mut key)
.map_err(|e| anyhow::anyhow!("Argon2 key derivation failed: {}", e))?;
let cipher = chacha20poly1305::ChaCha20Poly1305::new_from_slice(&key)
.map_err(|e| anyhow::anyhow!("Cipher init: {}", e))?;
let plaintext = cipher
.decrypt(
chacha20poly1305::aead::generic_array::GenericArray::from_slice(nonce),
ciphertext,
)
.map_err(|_| anyhow::anyhow!("Decryption failed — wrong passphrase"))?;
if plaintext.len() != 32 {
anyhow::bail!("Decrypted key is not 32 bytes");
}
// Write the restored key
fs::create_dir_all(identity_dir).await?;
let key_path = identity_dir.join("node_key");
fs::write(&key_path, &plaintext).await.context("Writing restored key")?;
// Set restrictive permissions
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o600);
std::fs::set_permissions(&key_path, perms)?;
}
// Derive DID and pubkey from the restored key
let signing_key = ed25519_dalek::SigningKey::from_bytes(
plaintext.as_slice().try_into().map_err(|_| anyhow::anyhow!("Invalid key"))?,
);
let pubkey = signing_key.verifying_key();
let pubkey_hex = hex::encode(pubkey.as_bytes());
let did = crate::identity::did_key_from_pubkey_hex(&pubkey_hex)?;
Ok((did, pubkey_hex))
}
+1 -1
View File
@@ -6,4 +6,4 @@
mod identity;
pub mod full;
pub use identity::create_encrypted_backup;
pub use identity::{create_encrypted_backup, restore_encrypted_backup};