1use m4a_seam::keyproof::Challenge;
6use serde_json::{json, Value};
7
8use crate::backend::wire::{field, Wire};
9use crate::error::{AgentError, Result};
10use crate::identity::{token_label, IdentityStore, SessionIdentity};
11
12pub const CHALLENGE_PATH: &str = "/product/v1/login/key/challenge";
13pub const ENROLL_PATH: &str = "/product/v1/enroll";
14pub const LOGIN_PATH: &str = "/product/v1/login/key";
15pub const LOGIN_TOKEN_PATH: &str = "/product/v1/login/key/token";
16
17pub fn refused_or(status: u16, what: &str) -> AgentError {
18 match status {
19 401 | 403 => AgentError::Refused(format!("{what}: the server did not accept the proof")),
20 s => AgentError::Protocol(format!("{what}: status {s}")),
21 }
22}
23
24pub fn parse_challenge(v: &Value) -> Result<Challenge> {
25 serde_json::from_value(v.clone()).map_err(|_| AgentError::Protocol("malformed challenge".into()))
26}
27
28pub fn fetch_challenge(wire: &dyn Wire, id: &SessionIdentity) -> Result<Challenge> {
30 let (st, v) = wire.post(CHALLENGE_PATH, &json!({ "key_id": id.key_id }), None)?;
31 if st != 200 {
32 return Err(refused_or(st, "challenge"));
33 }
34 parse_challenge(&v)
35}
36
37pub fn enroll(wire: &dyn Wire, ids: &IdentityStore, id: &mut SessionIdentity, invite: &str) -> Result<(String, String)> {
41 let aud = fetch_challenge(wire, id)?.audience;
43 let signature = id.sign_enroll(ids.vault(), &aud, invite)?;
44 let label = format!("m4a-agent:{}", id.session_id);
45 let mut body = json!({ "invite": invite, "public_key": id.public_key, "signature": signature, "label": label });
46 if let Some(nick) = id.requested_nick.as_deref().filter(|n| !n.is_empty()) {
47 body["nick"] = json!(nick);
48 }
49 let (st, v) = wire.post(ENROLL_PATH, &body, None)?;
50 if st == 400 || st == 409 || st == 429 {
51 let code = v.get("errcode").and_then(|c| c.as_str()).unwrap_or("");
53 return Err(AgentError::Refused(format!("enroll: the server refused the requested nick ({st} {code}); the invite is still good")));
54 }
55 if st != 200 {
56 return Err(refused_or(st, "enroll"));
57 }
58 let (nick, token) = (field(&v, "nick")?, field(&v, "token")?);
59 id.enrolled = true;
60 id.nick = Some(nick.clone());
61 ids.save(id)?;
62 ids.vault().put(&token_label(&id.session_id), token.as_bytes())?;
63 Ok((nick, token))
64}
65
66pub fn login(wire: &dyn Wire, ids: &IdentityStore, id: &SessionIdentity) -> Result<(String, String)> {
68 let c = fetch_challenge(wire, id)?;
69 let signature = id.sign_login(ids.vault(), &c)?;
70 let (st, v) = wire.post(LOGIN_PATH, &json!({ "key_id": id.key_id, "challenge_id": c.challenge_id, "signature": signature }), None)?;
71 if st != 200 {
72 return Err(refused_or(st, "login"));
73 }
74 let (nick, token) = (field(&v, "nick")?, field(&v, "token")?);
75 ids.vault().put(&token_label(&id.session_id), token.as_bytes())?;
76 Ok((nick, token))
77}
78
79pub fn login_token(wire: &dyn Wire, ids: &IdentityStore, id: &SessionIdentity) -> Result<String> {
83 let c = fetch_challenge(wire, id)?;
84 let signature = id.sign_login(ids.vault(), &c)?;
85 let (st, v) = wire.post(LOGIN_TOKEN_PATH, &json!({ "key_id": id.key_id, "challenge_id": c.challenge_id, "signature": signature }), None)?;
86 if st != 200 {
87 return Err(refused_or(st, "login token"));
88 }
89 field(&v, "login_token")
90}
91
92pub fn stored_token(ids: &IdentityStore, id: &SessionIdentity) -> Result<Option<String>> {
94 Ok(ids.vault().get(&token_label(&id.session_id))?.and_then(|b| String::from_utf8(b.to_vec()).ok()))
95}