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:
co-authored by
Claude Opus 4.6
parent
de8dcee155
commit
d1e14c4269
@@ -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))
|
||||
}
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
mod identity;
|
||||
pub mod full;
|
||||
|
||||
pub use identity::create_encrypted_backup;
|
||||
pub use identity::{create_encrypted_backup, restore_encrypted_backup};
|
||||
|
||||
Reference in New Issue
Block a user