Dorian c545b79b65 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>
2026-03-15 05:18:12 +00:00

133 lines
4.4 KiB
Rust

//! Encrypted DID identity backup for onboarding.
//! Uses Argon2 for key derivation and ChaCha20-Poly1305 for encryption.
use anyhow::{Context, Result};
use argon2::Argon2;
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use chacha20poly1305::aead::{Aead, KeyInit};
use rand::RngCore;
use std::path::Path;
use tokio::fs;
const BACKUP_VERSION: u32 = 1;
const SALT_LEN: usize = 16;
const NONCE_LEN: usize = 12;
const KEY_LEN: usize = 32;
/// Create an encrypted backup of the node identity key.
/// Returns JSON-serializable backup metadata + encrypted blob (base64).
pub async fn create_encrypted_backup(
identity_dir: &Path,
passphrase: &str,
did: &str,
pubkey_hex: &str,
) -> Result<serde_json::Value> {
let key_path = identity_dir.join("node_key");
let key_bytes = fs::read(&key_path)
.await
.context("Failed to read node key")?;
if key_bytes.len() != 32 {
anyhow::bail!("Invalid node key length");
}
let mut salt = [0u8; SALT_LEN];
let mut nonce = [0u8; NONCE_LEN];
rand::rngs::OsRng.fill_bytes(&mut salt);
rand::rngs::OsRng.fill_bytes(&mut nonce);
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 ciphertext = cipher
.encrypt(
chacha20poly1305::aead::generic_array::GenericArray::from_slice(&nonce),
key_bytes.as_ref(),
)
.map_err(|e| anyhow::anyhow!("Encryption failed: {}", e))?;
let mut blob = Vec::with_capacity(SALT_LEN + NONCE_LEN + ciphertext.len());
blob.extend_from_slice(&salt);
blob.extend_from_slice(&nonce);
blob.extend_from_slice(&ciphertext);
let blob_b64 = BASE64.encode(&blob);
Ok(serde_json::json!({
"version": BACKUP_VERSION,
"did": did,
"pubkey": pubkey_hex,
"kid": format!("{}#key-1", did),
"encrypted": true,
"blob": blob_b64,
"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))
}