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 #[builder(default)]
53 pub challenge: Vec<u8>,
54 #[builder(default)]
55 pub algorithms: Cow<'a, [PubKeyCredParams]>,
56 #[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 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 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 #[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 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}