lexe_connect/
requester.rs1use lexe_crypto::{hpke, rng::Crng};
4use lexe_uri::Uri;
5
6use crate::{
7 mailbox::Address,
8 request::{
9 CredentialRequest, CredentialRequestParams, Delivery, HpkePubkey,
10 OneTimeSecret, RequestError,
11 },
12 response::{CredentialResponse, CredentialResult, ResponseError},
13 seal::Blob,
14};
15
16#[derive(Debug, thiserror::Error)]
17pub enum AcceptError {
18 #[error("Redirect url has no response param")]
19 MissingResponse,
20 #[error("Response is not valid base64url: {0}")]
21 Base64(#[from] base64::DecodeError),
22 #[error("Response could not be decrypted: {0}")]
23 Decrypt(#[from] hpke::Error),
24 #[error(transparent)]
25 Response(#[from] ResponseError),
26 #[error("Response one_time_secret does not match the request")]
27 SecretMismatch,
28 #[error("Response account does not match the request")]
29 AccountMismatch,
30 #[error("Granted scopes or permissions differ from the request")]
31 GrantMismatch,
32}
33
34#[derive(Debug)]
38pub struct PendingRequest {
39 request: CredentialRequest,
40 ephemeral_key_pair: Option<hpke::KeyPair>,
41}
42
43impl PendingRequest {
44 pub fn new(
47 rng: &mut impl Crng,
48 params: CredentialRequestParams,
49 ) -> Result<Self, RequestError> {
50 let key_pair = hpke::KeyPair::from_rng(rng);
51 Self::build(rng, params, Some(key_pair))
52 }
53
54 pub fn new_plaintext(
57 rng: &mut impl Crng,
58 params: CredentialRequestParams,
59 ) -> Result<Self, RequestError> {
60 Self::build(rng, params, None)
61 }
62
63 fn build(
64 rng: &mut impl Crng,
65 params: CredentialRequestParams,
66 ephemeral_key_pair: Option<hpke::KeyPair>,
67 ) -> Result<Self, RequestError> {
68 let request = CredentialRequest {
69 ephemeral_hpke_pubkey: ephemeral_key_pair
70 .as_ref()
71 .map(|key_pair| HpkePubkey::from_hpke(key_pair.public_key())),
72 one_time_secret: OneTimeSecret::from_rng(rng),
73 params,
74 };
75 request.validate()?;
76
77 Ok(Self {
78 request,
79 ephemeral_key_pair,
80 })
81 }
82
83 pub fn request(&self) -> &CredentialRequest {
84 &self.request
85 }
86
87 pub fn connection_string(&self) -> String {
89 self.request.to_string()
90 }
91
92 pub fn mailbox(&self) -> Option<(&str, Address)> {
95 match &self.request.params.delivery {
96 Delivery::Mailbox(url) =>
97 Some((url, self.request.mailbox_address())),
98 _ => None,
99 }
100 }
101
102 pub fn accept_redirect(
104 &self,
105 url: &str,
106 ) -> Result<CredentialResponse, AcceptError> {
107 let encoded = Uri::parse(url)
108 .ok()
109 .and_then(|uri| {
110 uri.params
111 .into_iter()
112 .find(|param| param.key == "response")
113 .map(|param| param.value.into_owned())
114 })
115 .ok_or(AcceptError::MissingResponse)?;
116 let blob = Blob::from_base64url(&encoded)?;
117
118 self.accept_body(&blob.0)
119 }
120
121 pub fn accept_body(
130 &self,
131 body: &[u8],
132 ) -> Result<CredentialResponse, AcceptError> {
133 let response = match &self.ephemeral_key_pair {
134 Some(key_pair) =>
135 CredentialResponse::open(body, key_pair, &self.request)?,
136 None => CredentialResponse::from_json(body)?,
137 };
138
139 let request = &self.request;
140 if response.one_time_secret != request.one_time_secret {
141 return Err(AcceptError::SecretMismatch);
142 }
143 if response.account != request.params.account {
144 return Err(AcceptError::AccountMismatch);
145 }
146 if let CredentialResult::Granted(grant) = &response.result
147 && (grant.scopes != request.params.scopes
148 || grant.permissions != request.params.permissions)
149 {
150 return Err(AcceptError::GrantMismatch);
151 }
152
153 Ok(response)
154 }
155}