feat(lnd): capture aezeed at wallet init + encrypted backup + reveal UI
- aezeed words are now captured at BOTH wallet-init paths and stored encrypted (Argon2 + ChaCha20-Poly1305, per-node wallet secret) at identity/lnd_aezeed.enc; ack marker cleared when a wallet is recreated - lnd.init-wallet-from-seed was posting seed_entropy to /v1/initwallet, which is a GenSeed field — the wallet was never actually derived from the master seed; now GenSeed(entropy) → InitWallet(words) - new RPCs: lnd.seed-backup-status / lnd.seed-reveal (password + 2FA gated, same as seed.reveal via shared verify_reveal_auth) / lnd.seed-backup-ack - LND app detail page: Lightning-seed card with first-launch backup prompt, tap-to-reveal modal, 'I've backed it up' acknowledgment Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -505,11 +505,11 @@ async fn init_wallet_via_rest() -> Result<()> {
|
||||
anyhow::bail!("LND genseed returned no seed words");
|
||||
}
|
||||
|
||||
let wallet_password =
|
||||
base64::engine::general_purpose::STANDARD.encode(ensure_wallet_password().await?);
|
||||
let node_secret = ensure_wallet_password().await?;
|
||||
let wallet_password = base64::engine::general_purpose::STANDARD.encode(&node_secret);
|
||||
let req = InitWalletRequest {
|
||||
wallet_password,
|
||||
cipher_seed_mnemonic: seed.cipher_seed_mnemonic,
|
||||
cipher_seed_mnemonic: seed.cipher_seed_mnemonic.clone(),
|
||||
};
|
||||
match post_lnd_unlocker_json::<serde_json::Value>(
|
||||
&client,
|
||||
@@ -519,7 +519,14 @@ async fn init_wallet_via_rest() -> Result<()> {
|
||||
.await
|
||||
.context("initializing LND wallet")?
|
||||
{
|
||||
UnlockerResponse::Value(_) => {}
|
||||
UnlockerResponse::Value(_) => {
|
||||
persist_aezeed_backup(
|
||||
std::path::Path::new(ARCHY_DATA_DIR),
|
||||
&seed.cipher_seed_mnemonic,
|
||||
&node_secret,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
UnlockerResponse::WalletAlreadyExists => {
|
||||
unlock_existing_wallet().await?;
|
||||
}
|
||||
@@ -528,6 +535,32 @@ async fn init_wallet_via_rest() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Persist the just-created wallet's aezeed words encrypted with the per-node
|
||||
/// secret so they can be revealed later (`lnd.seed-reveal`). aezeed cannot be
|
||||
/// re-derived after init, so this is the only capture point on the unattended
|
||||
/// boot path. Best-effort: a failed backup must not fail wallet creation.
|
||||
pub(crate) async fn persist_aezeed_backup(
|
||||
data_dir: &std::path::Path,
|
||||
words: &[String],
|
||||
node_secret: &str,
|
||||
) {
|
||||
match crate::seed::save_lnd_aezeed_encrypted(data_dir, words, node_secret).await {
|
||||
Ok(()) => {
|
||||
// A fresh wallet means a fresh seed — any previous "user backed it
|
||||
// up" acknowledgment no longer applies.
|
||||
crate::seed::clear_lnd_aezeed_acknowledged(data_dir).await;
|
||||
tracing::info!("[lnd] aezeed backup saved (encrypted with per-node secret)");
|
||||
}
|
||||
Err(e) => tracing::warn!("[lnd] failed to save aezeed backup: {e:#}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The per-node wallet password if one has been persisted; never generates.
|
||||
/// Used by the reveal RPC to decrypt the aezeed backup.
|
||||
pub(crate) async fn wallet_password_if_exists() -> Option<String> {
|
||||
read_wallet_password().await
|
||||
}
|
||||
|
||||
async fn get_lnd_unlocker_json<T: for<'de> Deserialize<'de>>(
|
||||
client: &reqwest::Client,
|
||||
path: &str,
|
||||
|
||||
Reference in New Issue
Block a user