wasm_webauthn/
lib.rs

1use std::{borrow::Cow, io::Read, ops::Deref};
2
3use coset::{CborSerializable, CoseKey};
4use derive_builder::Builder;
5use js_sys::{Array, Uint8Array};
6use serde::{Deserialize, Serialize};
7
8mod error;
9
10pub use error::*;
11use tracing::{debug, instrument, trace};
12use wasm_bindgen::JsValue;
13use wasm_bindgen_futures::JsFuture;
14pub use web_sys::UserVerificationRequirement;
15use web_sys::{
16    window, AuthenticatorAssertionResponse, AuthenticatorAttestationResponse,
17    AuthenticatorSelectionCriteria, CredentialCreationOptions, CredentialRequestOptions,
18    PublicKeyCredential, PublicKeyCredentialCreationOptions, PublicKeyCredentialDescriptor,
19    PublicKeyCredentialRequestOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity,
20    Window,
21};
22
23#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize)]
24#[serde(tag = "type", rename = "public-key")]
25pub struct PubKeyCredParams {
26    alg: i32,
27}
28
29impl PubKeyCredParams {
30    const fn nistp256() -> Self {
31        Self { alg: -7 }
32    }
33}
34impl Default for PubKeyCredParams {
35    fn default() -> Self {
36        Self::nistp256()
37    }
38}
39
40#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize)]
41#[serde(rename_all = "lowercase")]
42pub enum UserVerification {
43    Required,
44    Preferred,
45    Discouraged,
46}
47
48#[derive(Debug, Clone, Builder)]
49pub struct MakeCredentialArgs<'a> {
50    /// Challenge to be included within `clientDataJson`
51    /// only relevant if attestion is requested
52    #[builder(default)]
53    pub challenge: Vec<u8>,
54    #[builder(default)]
55    pub algorithms: Cow<'a, [PubKeyCredParams]>,
56    /// A string which indicates the relying party's identifier (ex. "login.example.org"). If this option is not provided, the client will use the current origin's domain.
57    #[builder(default)]
58    pub rp_id: Option<String>,
59    #[builder(default = "UserVerificationRequirement::Discouraged")]
60    pub uv: UserVerificationRequirement,
61    #[builder(default)]
62    pub resident_key: bool,
63    #[builder(default)]
64    pub timeout: Option<u32>,
65    #[builder(default)]
66    pub user_id: Option<Vec<u8>>,
67    #[builder(default)]
68    pub user_name: Option<String>,
69    #[builder(default)]
70    pub user_display_name: Option<String>,
71}
72#[instrument(skip(reader))]
73fn read_fixed<const N: usize, R: Read>(mut reader: R) -> Result<[u8; N]> {
74    let mut arr = [0u8; N];
75    let res = reader.read_exact(&mut arr[..]);
76    trace!(%N, ?res, "read");
77    Ok(arr)
78}
79
80#[instrument(skip(reader))]
81fn read_vec<R: Read>(mut reader: R) -> Result<Vec<u8>> {
82    let len: [u8; 2] = read_fixed(&mut reader)?;
83    let len = u16::from_be_bytes(len) as usize;
84    let mut vec = vec![0u8; len];
85    let res = reader.read_exact(&mut vec[..]);
86    trace!(%len, ?res, "read");
87    Ok(vec)
88}
89
90impl MakeCredentialArgs<'_> {
91    pub async fn make_credential(&self) -> Result<MakeCredentialResponse> {
92        let window = get_window()?;
93        let navigator = window.navigator();
94        let challenge = Uint8Array::from(self.challenge.as_slice());
95        let default_alg = &[PubKeyCredParams::nistp256()][..];
96        let algorithms = serde_wasm_bindgen::to_value(if self.algorithms.is_empty() {
97            default_alg
98        } else {
99            &self.algorithms
100        })?;
101        let mut options = CredentialCreationOptions::new();
102        let user = PublicKeyCredentialUserEntity::new(
103            self.user_name.as_deref().unwrap_or_default(),
104            self.user_display_name.as_deref().unwrap_or_default(),
105            &Uint8Array::from(self.user_id.as_deref().unwrap_or(&[0u8])).into(),
106        );
107        let mut selection = AuthenticatorSelectionCriteria::new();
108        selection.require_resident_key(self.resident_key);
109        selection.user_verification(self.uv);
110        let rp = PublicKeyCredentialRpEntity::new(&match self.rp_id.as_deref() {
111            Some(rp) => Cow::Borrowed(rp),
112            None => Cow::Owned(window.location().hostname()?),
113        });
114        options.public_key(
115            PublicKeyCredentialCreationOptions::new(&challenge, &algorithms, &rp, &user)
116                .authenticator_selection(&selection),
117        );
118        // Request credential
119        let response =
120            JsFuture::from(navigator.credentials().create_with_options(&options)?).await?;
121        let public_key_response = PublicKeyCredential::from(response);
122
123        let attestation_response =
124            AuthenticatorAttestationResponse::from(JsValue::from(public_key_response.response()));
125        let attestation_object: AttestationObject = ciborium::de::from_reader(
126            &Uint8Array::new(&attestation_response.attestation_object()).to_vec()[..],
127        )?;
128        let (id, public_key) = {
129            let mut reader = &attestation_object.auth_data[..];
130            //TODO: return
131            let _rp_id_hash: [u8; 32] = read_fixed(&mut reader)?;
132            let _flags = read_fixed::<1, _>(&mut reader)?[0];
133            let _counter = u32::from_be_bytes(read_fixed::<4, _>(&mut reader)?);
134            let _aaguid = read_fixed::<16, _>(&mut reader)?;
135
136            let id = CredentialID(read_vec(&mut reader)?);
137            let public_key = CoseKey::from_slice(reader)?;
138            (id, public_key)
139        };
140        let credential = Credential {
141            id,
142            public_key: Some(public_key),
143        };
144        Ok(MakeCredentialResponse { credential })
145    }
146}
147
148fn get_window() -> Result<Window> {
149    window().ok_or(Error::ContextUnavailable)
150}
151#[derive(Deserialize)]
152#[serde(tag = "fmt", rename = "packed")]
153struct AttestationObject {
154    #[serde(rename = "authData", with = "serde_bytes")]
155    auth_data: Vec<u8>,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct CredentialID(pub Vec<u8>);
160
161impl Deref for CredentialID {
162    type Target = Vec<u8>;
163    fn deref(&self) -> &Self::Target {
164        &self.0
165    }
166}
167
168#[derive(Debug, Clone)]
169pub struct Credential {
170    pub id: CredentialID,
171    pub public_key: Option<CoseKey>,
172}
173
174impl From<CredentialID> for Credential {
175    fn from(id: CredentialID) -> Self {
176        Self {
177            id,
178            public_key: None,
179        }
180    }
181}
182
183pub struct MakeCredentialResponse {
184    pub credential: Credential,
185}
186
187#[derive(Debug, Builder)]
188pub struct GetAssertionArgs {
189    /// List of credentials, will attempt to use an resident key if `None` is specified
190    #[builder(default)]
191    pub credentials: Option<Vec<Credential>>,
192    #[builder(default)]
193    pub rp_id: Option<String>,
194    #[builder(default = "UserVerificationRequirement::Discouraged")]
195    pub uv: UserVerificationRequirement,
196    #[builder(default)]
197    pub timeout: Option<u32>,
198    #[builder(default)]
199    pub challenge: Vec<u8>,
200}
201
202impl GetAssertionArgs {
203    pub async fn get_assertion(&self) -> Result<GetAssertionResponse> {
204        let window = get_window()?;
205        let mut request_options =
206            PublicKeyCredentialRequestOptions::new(&Uint8Array::from(self.challenge.as_slice()));
207        request_options.rp_id(&match self.rp_id.as_deref() {
208            Some(rp) => Cow::Borrowed(rp),
209            None => Cow::Owned(window.location().hostname()?),
210        });
211        if let Some(ref credentials) = self.credentials {
212            request_options.allow_credentials(&JsValue::from(Array::from_iter(
213                credentials.iter().map(|Credential { id, .. }| {
214                    PublicKeyCredentialDescriptor::new(
215                        &Uint8Array::from(id.as_slice()),
216                        web_sys::PublicKeyCredentialType::PublicKey,
217                    )
218                }),
219            )));
220        }
221        request_options.user_verification(self.uv);
222        if let Some(timeout) = self.timeout {
223            request_options.timeout(timeout);
224        }
225
226        let mut options = CredentialRequestOptions::new();
227        options.public_key(&request_options);
228
229        fn dbg<T: std::fmt::Debug>(v: T) -> T {
230            debug!(value = ?v, "dbg");
231            v
232        }
233
234        let response = dbg(JsFuture::from(
235            window
236                .navigator()
237                .credentials()
238                .get_with_options(&options)?,
239        )
240        .await)?;
241
242        let public_key_response = PublicKeyCredential::from(response);
243
244        let assertion_response =
245            AuthenticatorAssertionResponse::from(JsValue::from(&public_key_response.response()));
246        let authenticator_data = Uint8Array::new(&assertion_response.authenticator_data()).to_vec();
247        let signature = Uint8Array::new(&assertion_response.signature()).to_vec();
248        let client_data_json =
249            String::from_utf8(Uint8Array::new(&assertion_response.client_data_json()).to_vec())?;
250        let (flags, counter) = {
251            let mut reader = authenticator_data.as_slice();
252            let _rp_id_hash = read_fixed::<32, _>(&mut reader)?;
253            let flags = read_fixed::<1, _>(&mut reader)?[0];
254            let counter = u32::from_be_bytes(read_fixed::<4, _>(&mut reader)?);
255            (flags, counter)
256        };
257        // TODO: read credential ID which has been used
258        Ok(GetAssertionResponse {
259            signature,
260            client_data_json,
261            flags,
262            counter,
263        })
264    }
265}
266
267#[derive(Debug, Clone, PartialEq, Eq)]
268pub struct GetAssertionResponse {
269    pub signature: Vec<u8>,
270    pub client_data_json: String,
271    pub flags: u8,
272    pub counter: u32,
273}