use agent_mesh_protocol::Fingerprint;
use base64::engine::general_purpose::STANDARD as B64;
use base64::Engine as _;
use crate::enrollment::EnrollmentCandidate;
use crate::sas_transcript::{sas_words, TranscriptInputs, SAS_WORD_COUNT};
use crate::tty::PromptWindow;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SasVerdict {
Confirmed,
Declined,
NoTerminal,
TranscriptMismatch,
}
impl SasVerdict {
#[must_use]
pub fn is_confirmed(self) -> bool {
matches!(self, Self::Confirmed)
}
}
#[must_use]
pub fn recompute_sas(
candidate: &EnrollmentCandidate,
issuer: &str,
subject: &str,
) -> Option<[&'static str; SAS_WORD_COUNT]> {
let cose_pubkey = B64.decode(&candidate.cose_pubkey).ok()?;
let enroll_nonce = B64.decode(&candidate.enroll_nonce).ok()?;
let commitment = candidate.commitment.parse::<Fingerprint>().ok()?;
let transcript = TranscriptInputs {
rp_id: &candidate.rp_id,
issuer,
subject,
mesh_agent_fingerprint: &candidate.mesh_agent_fingerprint,
cose_alg: candidate.cose_alg,
cose_pubkey: &cose_pubkey,
commitment: &commitment,
enroll_nonce: &enroll_nonce,
}
.transcript_id();
(transcript.hex() == candidate.transcript_id).then(|| sas_words(&transcript))
}
#[must_use]
pub fn confirm_prompt(words: &[&str; SAS_WORD_COUNT]) -> String {
format!(
"compare code {} matches the browser? [y/N] ",
words.join(" ")
)
}
#[must_use]
pub fn answer_verdict(answer: Option<&str>) -> SasVerdict {
match answer {
Some(text) if matches!(text.trim().to_ascii_lowercase().as_str(), "y" | "yes") => {
SasVerdict::Confirmed
}
_ => SasVerdict::Declined,
}
}
pub fn confirm_enrollment(
window: Option<&PromptWindow>,
candidate: &EnrollmentCandidate,
issuer: &str,
subject: &str,
) -> SasVerdict {
let Some(window) = window else {
return SasVerdict::NoTerminal;
};
let Some(words) = recompute_sas(candidate, issuer, subject) else {
return SasVerdict::TranscriptMismatch;
};
if window.ask(&confirm_prompt(&words)).is_err() {
return SasVerdict::Declined;
}
let mut answer = String::new();
match window.read_line_into(&mut answer) {
Ok(0) | Err(_) => answer_verdict(None),
Ok(_) => answer_verdict(Some(&answer)),
}
}