Implement onboarding reset functionality and enhance backup features

- Added a new method to reset the onboarding state, allowing users to re-initiate the onboarding process.
- Integrated backup creation functionality, enabling users to create encrypted backups of their node identity.
- Updated API endpoints to handle onboarding reset and backup creation requests.
- Enhanced UI components to support the new onboarding reset and backup features, including error handling and user feedback.
- Introduced new dependencies for cryptographic operations and data encoding.
This commit is contained in:
Dorian
2026-03-02 08:34:13 +00:00
parent 94eb1e4283
commit 62d6c13764
23 changed files with 559 additions and 88 deletions
+53
View File
@@ -1,4 +1,5 @@
use crate::auth::AuthManager;
use crate::backup;
use crate::config::Config;
use crate::container::docker_packages;
use crate::container::DevContainerOrchestrator;
@@ -88,6 +89,7 @@ impl RpcHandler {
"auth.changePassword" => self.handle_auth_change_password(rpc_req.params).await,
"auth.onboardingComplete" => self.handle_auth_onboarding_complete().await,
"auth.isOnboardingComplete" => self.handle_auth_is_onboarding_complete().await,
"auth.resetOnboarding" => self.handle_auth_reset_onboarding().await,
// Container orchestration (for Archipelago-managed containers)
"container-install" => self.handle_container_install(rpc_req.params).await,
@@ -119,6 +121,8 @@ impl RpcHandler {
"node-messages-received" => self.handle_node_messages_received().await,
"node-nostr-discover" => self.handle_node_nostr_discover().await,
"node.did" => self.handle_node_did().await,
"node.signChallenge" => self.handle_node_sign_challenge(rpc_req.params).await,
"node.createBackup" => self.handle_node_create_backup(rpc_req.params).await,
"node.tor-address" => self.handle_node_tor_address().await,
"node.nostr-publish" => self.handle_node_nostr_publish().await,
"node.nostr-pubkey" => self.handle_node_nostr_pubkey().await,
@@ -239,12 +243,61 @@ impl RpcHandler {
Ok(serde_json::json!(complete))
}
async fn handle_auth_reset_onboarding(&self) -> Result<serde_json::Value> {
self.auth_manager.reset_onboarding().await?;
Ok(serde_json::json!(true))
}
async fn handle_node_did(&self) -> Result<serde_json::Value> {
let (data, _) = self.state_manager.get_snapshot().await;
let did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?;
Ok(serde_json::json!({ "did": did, "pubkey": data.server_info.pubkey }))
}
/// Sign a challenge to prove control of the node DID (proof-of-control for onboarding).
async fn handle_node_sign_challenge(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let challenge = params
.get("challenge")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing challenge string"))?;
let identity_dir = self.config.data_dir.join("identity");
let identity = identity::NodeIdentity::load_or_create(&identity_dir).await?;
let signature = identity.sign(challenge.as_bytes());
Ok(serde_json::json!({ "signature": signature }))
}
/// Create an encrypted backup of the node identity (for onboarding).
async fn handle_node_create_backup(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let passphrase = params
.get("passphrase")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing passphrase"))?;
let (data, _) = self.state_manager.get_snapshot().await;
let did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?;
let identity_dir = self.config.data_dir.join("identity");
let backup = backup::create_encrypted_backup(
&identity_dir,
passphrase,
&did,
&data.server_info.pubkey,
)
.await?;
Ok(backup)
}
async fn handle_node_tor_address(&self) -> Result<serde_json::Value> {
let tor_address = docker_packages::read_tor_address("archipelago");
Ok(serde_json::json!({ "tor_address": tor_address }))
+18
View File
@@ -86,6 +86,24 @@ impl AuthManager {
Ok(())
}
/// Reset onboarding state so the user can go through onboarding again (dev/testing).
pub async fn reset_onboarding(&self) -> Result<()> {
let onboarding_file = self.data_dir.join("onboarding.json");
let state = OnboardingState { complete: false };
fs::write(
&onboarding_file,
serde_json::to_string_pretty(&state)?,
)
.await?;
if let Some(mut user) = self.get_user().await? {
user.onboarding_complete = false;
let user_file = self.data_dir.join("user.json");
let content = serde_json::to_string_pretty(&user)?;
fs::write(&user_file, content).await?;
}
Ok(())
}
pub async fn is_onboarding_complete(&self) -> Result<bool> {
// Check onboarding.json first (persisted before user setup)
let onboarding_file = self.data_dir.join("onboarding.json");
+68
View File
@@ -0,0 +1,68 @@
//! 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(),
}))
}
+1
View File
@@ -7,6 +7,7 @@ use tracing::info;
mod api;
mod auth;
mod backup;
mod config;
mod electrs_status;
mod container;