//! 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 { 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(), })) }