- Byte-exact TCE codec (encoder/decoder, canonical numbers, vectors-tested) - Ed25519 identities with bech32m trust1… addresses - Relay client: challenge/auth handshake, request feed, responses, transparent re-auth on 401 - iced GUI (Material-dark, Solana-style palette): tab shell, approval prompts with native toasts on Linux, address book, history, settings, password change - Portable single-file vault (identity + book + history + settings) - Live 2FA roundtrip examples (send_2fa, await_2fa)
103 lines
3.7 KiB
Rust
103 lines
3.7 KiB
Rust
//! Full 2FA service flow: post an approval request, then poll for the user's
|
|
//! decision until the request window lapses.
|
|
//!
|
|
//! Usage:
|
|
//! cargo run --example await_2fa -- [relay] <recipient-address> [action] [message]
|
|
//!
|
|
//! Exit code: 0 = approved, 4 = denied, 5 = timeout.
|
|
|
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
|
|
|
use niko_trust_gui::address::Address;
|
|
use niko_trust_gui::protocol::{encode_approval_request, ApprovalRequest};
|
|
use niko_trust_gui::relay::{object_hash, request_id_hex, Relay};
|
|
use niko_trust_gui::signer::Signer;
|
|
use niko_trust_gui::tce::{MAX_APPROVAL_LIFETIME, NONCE_SIZE};
|
|
|
|
fn main() {
|
|
let mut args = std::env::args().skip(1);
|
|
let first = args.next().unwrap_or_default();
|
|
let (base, addr_s) = if first.starts_with("trust1") {
|
|
("https://trust.n1ko.dev".to_string(), first)
|
|
} else {
|
|
let a = args.next().unwrap_or_else(|| {
|
|
eprintln!("usage: await_2fa [relay] <recipient-address> [action] [message]");
|
|
std::process::exit(2);
|
|
});
|
|
(first.trim_end_matches('/').to_string(), a)
|
|
};
|
|
let action = args.next().unwrap_or_else(|| "2fa".to_string());
|
|
let message = args
|
|
.next()
|
|
.unwrap_or_else(|| "Confirm sign-in (2FA)".to_string());
|
|
|
|
let recipient = Address::parse(&addr_s).expect("invalid recipient address");
|
|
let sender = Signer::generate().expect("generate sender key");
|
|
|
|
let created = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.expect("clock")
|
|
.as_secs();
|
|
let mut nonce = [0u8; NONCE_SIZE];
|
|
getrandom::fill(&mut nonce).expect("rng");
|
|
|
|
let req = ApprovalRequest {
|
|
sender: sender.pubkey(),
|
|
recipient: *recipient.pubkey(),
|
|
action,
|
|
payload: Default::default(),
|
|
message,
|
|
created_at: created,
|
|
expires_at: created + MAX_APPROVAL_LIFETIME,
|
|
nonce,
|
|
};
|
|
let tce = encode_approval_request(&req).expect("encode request");
|
|
let sig = sender.sign(&tce);
|
|
let req_hash = object_hash(&tce);
|
|
|
|
// The "service" authenticates and stores the request.
|
|
let agent = Relay::auth(&base, &sender, "*").expect("sender auth");
|
|
let id = agent.store(&tce, &sig).expect("store request");
|
|
println!(
|
|
"request {} stored (id {}), waiting up to {MAX_APPROVAL_LIFETIME}s for a decision…",
|
|
req.action, id
|
|
);
|
|
|
|
// Poll for the response until the window lapses.
|
|
let rid = request_id_hex(&tce);
|
|
let deadline = Instant::now() + Duration::from_secs(MAX_APPROVAL_LIFETIME + 15);
|
|
while Instant::now() < deadline {
|
|
std::thread::sleep(Duration::from_secs(2));
|
|
let responses = match agent.fetch_responses(&rid) {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
eprintln!("poll error: {e}");
|
|
continue;
|
|
}
|
|
};
|
|
for inc in responses {
|
|
if inc.response.request_hash != req_hash {
|
|
continue; // answer to some other request
|
|
}
|
|
if inc.response.responder != *recipient.pubkey() {
|
|
eprintln!(
|
|
"warning: response from {}, expected {}",
|
|
inc.responder, recipient
|
|
);
|
|
continue;
|
|
}
|
|
match inc.response.decision {
|
|
niko_trust_gui::protocol::Decision::Allow => {
|
|
println!("DECISION: APPROVED by {}", inc.responder);
|
|
std::process::exit(0);
|
|
}
|
|
niko_trust_gui::protocol::Decision::Deny => {
|
|
println!("DECISION: DENIED by {}", inc.responder);
|
|
std::process::exit(4);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
println!("DECISION: TIMEOUT (no answer within the request window)");
|
|
std::process::exit(5);
|
|
}
|