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:
@@ -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 }))
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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(),
|
||||
}))
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use tracing::info;
|
||||
|
||||
mod api;
|
||||
mod auth;
|
||||
mod backup;
|
||||
mod config;
|
||||
mod electrs_status;
|
||||
mod container;
|
||||
|
||||
Reference in New Issue
Block a user