fix(nip46): restored sessions no longer demand a connect secret re-echo
Live Amber sign-in (Sep 25) paired fine, but every restart died with 'the signer did not echo the connection secret': the restore re-sends connect with the ORIGINAL pairing secret, and an already-approved signer legitimately answers 'true' without re-echoing — NIP-46 reserves the echo for proving possession during the INITIAL pairing, and Amber proved it once. Requiring it on re-dial made session restore fail 100% against real Amber (the e2e missed it because its fake answered a plain ack with no secret in play). ConnectUri gains a 'restore' flag (set only by reactivate_saved_sessions). Fresh handshakes still fail closed on a wrong echo. The restore path's anti-spoofing is expected_identity in adopt_identity — a peer answering as any other account is refused, and only the real key holder can decrypt traffic on the stored conversation key. Log now says 'secret validation: SKIPPED (restored session)' and proceeds. e2e: run_fake_amber now mirrors Amber's ack shapes (plain ack on first pairing; 'true' when the client re-presents a secret) and the restore test seeds a pairing secret so it exercises exactly the live failure — it fails without the fix and passes with it. cargo test 216 unit + 5 e2e green; clippy --all-targets 0 warnings; fmt clean; release rebuilt.
This commit is contained in:
parent
929d791240
commit
01ce5de417
2 changed files with 63 additions and 5 deletions
|
|
@ -284,9 +284,18 @@ async fn run_fake_amber(relay_url: String, comms: Keys, identity: Keys, approval
|
|||
|
||||
let response: Value = match method {
|
||||
"connect" => {
|
||||
// Simulate a human tapping "approve" in Amber.
|
||||
// Simulate a human tapping "approve" in Amber. Amber's
|
||||
// ack SHAPE depends on the connection state: a first-time
|
||||
// pairing (no secret in params) gets a plain ack; an
|
||||
// ALREADY-APPROVED connection re-dialing with the stored
|
||||
// secret gets `true` WITHOUT a secret echo (live Amber,
|
||||
// Sep 25 — the client must not demand an echo there).
|
||||
tokio::time::sleep(approval_delay).await;
|
||||
json!({"id": id, "result": "ack"})
|
||||
if req["params"].as_array().is_some_and(|p| p.len() > 1) {
|
||||
json!({"id": id, "result": true})
|
||||
} else {
|
||||
json!({"id": id, "result": "ack"})
|
||||
}
|
||||
}
|
||||
"get_public_key" => json!({"id": id, "result": identity.public_key().to_hex()}),
|
||||
"sign_event" => {
|
||||
|
|
@ -1189,6 +1198,18 @@ async fn nip46_session_restore_redials_and_refuses_wrong_identity() {
|
|||
// (The old instance's listener task is left running on purpose — the
|
||||
// live process exiting is modeled by the new instance, not by
|
||||
// `disconnect()`, which revokes and wipes the stored key.)
|
||||
//
|
||||
// Seed a pairing secret at the identity ref first: a QR pairing stores
|
||||
// one, and the restore re-sends it — real Amber then answers `true`
|
||||
// WITHOUT echoing (already-approved connection), which the fake models.
|
||||
// Without the restore-mode skip this handshake fails "did not echo the
|
||||
// connection secret" (the exact live Sep 25 failure).
|
||||
{
|
||||
let mut g = app.lock().await;
|
||||
keynectr::vault::store_connection_secret(&mut g.vault, None, &ref_id, "restore-secret-123")
|
||||
.unwrap();
|
||||
g.save_vault().unwrap();
|
||||
}
|
||||
let signer2 = Nip46ClientSigner::new(app.clone());
|
||||
let restored = signer2
|
||||
.reactivate_saved_sessions()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue