use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result};
use console::{Emoji, style};
use indicatif::{ProgressBar, ProgressStyle};
use auths_sdk::core_config::EnvironmentConfig;
use auths_sdk::pairing::PairingSession;
use auths_sdk::pairing::SubmitResponseRequest;
use auths_sdk::signing::PassphraseProvider;
use crate::core::fs::{create_restricted_dir, write_sensitive_file};
pub(crate) static LOCK: Emoji<'_, '_> = Emoji("🔐 ", "");
pub(crate) static LINK: Emoji<'_, '_> = Emoji("🔗 ", "");
pub(crate) static CHECK: Emoji<'_, '_> = Emoji("✅ ", "[OK] ");
pub(crate) static PHONE: Emoji<'_, '_> = Emoji("📱 ", "");
pub(crate) static GEAR: Emoji<'_, '_> = Emoji("⚙️ ", "");
pub(crate) static WARN: Emoji<'_, '_> = Emoji("⚠️ ", "[!] ");
pub(crate) fn create_wait_spinner(message: &str) -> ProgressBar {
let pb = ProgressBar::new_spinner();
#[allow(clippy::unwrap_used)] pb.set_style(
ProgressStyle::with_template("{spinner:.cyan} {msg}")
.unwrap()
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
);
pb.set_message(message.to_string());
pb.enable_steady_tick(Duration::from_millis(80));
pb
}
pub(crate) fn print_pairing_header(mode: &str, registry: &str, controller_did: &str) {
println!();
println!(
"{}",
style(format!("━━━ {LOCK}Auths Device Pairing ({mode}) ━━━")).bold()
);
println!();
println!(" {} {}", style("Registry:").dim(), style(registry).cyan());
println!(
" {} {}",
style("Identity:").dim(),
style(controller_did).cyan()
);
println!();
}
pub(crate) fn print_completion(device_name: Option<&str>, device_did: &str) {
println!();
let label = device_name.unwrap_or("new device");
println!(
"{}Paired with {} {}",
CHECK,
style(label).bold(),
style(format!("({device_did})")).dim()
);
println!();
}
pub(crate) fn prompt_sas_confirmation(sas_bytes: &[u8; 10]) -> Result<bool> {
use auths_pairing_protocol::sas;
println!();
println!("{}", style(format!("━━━ {LOCK}Verify Pairing ━━━")).bold());
println!();
println!(" Confirm this code matches your other device:");
println!();
println!(" {}", style(sas::format_sas_emoji(sas_bytes)).bold());
println!(
" {}",
style(format!("({})", sas::format_sas_numeric(sas_bytes))).dim()
);
println!();
println!(
" {}",
style("If the codes don't match, someone may be intercepting this connection.").dim()
);
println!();
let confirmed = dialoguer::Confirm::new()
.with_prompt("Do the codes match? [y/N]")
.default(false)
.interact()
.context("Failed to read confirmation")?;
Ok(confirmed)
}
pub(crate) fn display_sas_mismatch_warning() {
println!();
println!(
" {}{}",
WARN,
style("PAIRING ABORTED — possible interception detected")
.red()
.bold()
);
println!();
println!(" The verification codes did not match. This could mean:");
println!(" • An attacker is intercepting the connection (MITM)");
println!(" • A network issue corrupted the key exchange");
println!();
println!(" Retry on a trusted network. If this persists, do not pair these devices.");
println!(" No keys or attestations were created.");
println!();
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn handle_pairing_response(
now: chrono::DateTime<chrono::Utc>,
session: &mut PairingSession,
response: SubmitResponseRequest,
auths_dir: &Path,
capabilities: &[auths_keri::Capability],
passphrase_provider: Arc<dyn PassphraseProvider + Send + Sync>,
env_config: &EnvironmentConfig,
verify_sas: bool,
) -> Result<()> {
use auths_sdk::pairing;
use crate::factories::storage::build_auths_context;
println!();
println!(
"{}",
style(format!("━━━ {LINK}Response Received ━━━"))
.bold()
.cyan()
);
println!();
if let Some(name) = &response.device_name {
println!(
" {} {}",
style(format!("{PHONE}Device:")).dim(),
style(name).bold()
);
}
println!(
" {} {}",
style("DID:").dim(),
style(&response.device_did).dim()
);
println!();
let device_ecdh_bytes = response
.device_ephemeral_pubkey
.decode()
.context("Invalid ephemeral pubkey encoding")?;
let device_signing_bytes = response
.device_signing_pubkey
.decode()
.context("Invalid signing pubkey encoding")?;
let signature_bytes = response
.signature
.decode()
.context("Invalid signature encoding")?;
let verify_spinner = create_wait_spinner(&format!("{GEAR}Verifying signature..."));
let curve: auths_crypto::CurveType = response.curve.into();
session
.verify_response(
&device_signing_bytes,
&device_ecdh_bytes,
&signature_bytes,
curve,
)
.context("Signature verification failed")?;
verify_spinner.finish_with_message(format!("{CHECK}Signature verified"));
let exchange_spinner = create_wait_spinner(&format!("{GEAR}Completing key exchange..."));
let initiator_ecdh_pub = session
.ephemeral_pubkey_bytes()
.context("Failed to get initiator pubkey")?;
let shared_secret = session
.complete_exchange(&device_ecdh_bytes)
.context("ECDH key exchange failed")?;
exchange_spinner.finish_with_message(format!("{CHECK}Key exchange complete"));
let session_id = &session.token.session_id;
let short_code = &session.token.short_code;
let sas_bytes = auths_pairing_protocol::sas::derive_sas(
&shared_secret,
&initiator_ecdh_pub,
&device_ecdh_bytes,
session_id,
short_code,
);
let transport_key = auths_pairing_protocol::sas::derive_transport_key(
&shared_secret,
&initiator_ecdh_pub,
&device_ecdh_bytes,
session_id,
short_code,
);
if verify_sas {
let confirmed = prompt_sas_confirmation(&sas_bytes)?;
if !confirmed {
display_sas_mismatch_warning();
drop(transport_key);
anyhow::bail!("SAS verification failed — pairing aborted");
}
} else {
use auths_pairing_protocol::sas;
println!(
" {} {} {}",
style("SAS:").dim(),
sas::format_sas_emoji(&sas_bytes),
style(format!("({})", sas::format_sas_numeric(&sas_bytes))).dim()
);
}
if !auths_dir.exists() {
println!();
println!(
" {}{}",
WARN,
style("No local identity found at ~/.auths").yellow()
);
println!(" Run 'auths init' first to create an identity.");
save_device_info(now, auths_dir, &response)?;
return Ok(());
}
if response.responder_inception_event.is_empty() {
println!();
println!(
" {}{}",
WARN,
style("The joining device did not send a delegated inception (dip).").yellow()
);
println!(
" {}",
style("Update the joining client to pair as a KERI-delegated device.").dim()
);
save_device_info(now, auths_dir, &response)?;
return Ok(());
}
let _ = capabilities;
let anchor_spinner = create_wait_spinner(&format!("{GEAR}Anchoring delegated device..."));
let ctx = build_auths_context(auths_dir, env_config, Some(passphrase_provider))
.context("Failed to build auths context")?;
let anchor = pairing::anchor_pairing_response(
&ctx,
&response.responder_inception_event,
response.device_name.clone(),
)
.map_err(anyhow::Error::from)
.context("Failed to anchor the delegated device")?;
anchor_spinner.finish_with_message(format!("{CHECK}Delegated device anchored"));
print_completion(anchor.device_name.as_deref(), &anchor.device_did);
Ok(())
}
pub(crate) fn save_device_info(
now: chrono::DateTime<chrono::Utc>,
auths_dir: &Path,
response: &SubmitResponseRequest,
) -> Result<()> {
let devices_dir = auths_dir.join("devices");
create_restricted_dir(&devices_dir)?;
let device_file = devices_dir.join(format!(
"{}.json",
&response.device_signing_pubkey[..8.min(response.device_signing_pubkey.len())]
));
let device_info = serde_json::json!({
"device_did": response.device_did.as_str(),
"signing_pubkey": response.device_signing_pubkey.as_str(),
"x25519_pubkey": response.device_ephemeral_pubkey.as_str(),
"name": response.device_name,
"paired_at": now.to_rfc3339(),
});
write_sensitive_file(&device_file, serde_json::to_string_pretty(&device_info)?)?;
println!(
" {}{}",
CHECK,
style(format!("Device info saved to: {}", device_file.display())).dim()
);
Ok(())
}
#[allow(clippy::disallowed_methods)] pub(crate) fn hostname() -> String {
std::env::var("HOSTNAME")
.or_else(|_| std::env::var("HOST"))
.unwrap_or_else(|_| "unknown-device".to_string())
}