Skip to content

Commit 5d8da41

Browse files
committed
wip: Sketch out hybrid QR transport support
1 parent abf258b commit 5d8da41

11 files changed

Lines changed: 753 additions & 37 deletions

File tree

xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ async-trait = "0.1.88"
2525
tokio = { version = "1.45.0", features = ["rt-multi-thread"] }
2626
futures-lite = "2.6.0"
2727

28+
qrcode = "0.14.1"
2829
# this is temporary until we move COSE -> Vec<u8> serialization methods into libwebauthn
2930
cosey = "0.3.2"
3031
rustls = { version = "0.23.27", default-features = false, features = ["std", "tls12", "ring", "log", "logging", "prefer-post-quantum"] }

xyz-iinuwa-credential-manager-portal-gtk/data/resources/ui/window.ui

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,51 @@
119119
</object>
120120
</child>
121121

122+
<child>
123+
<object class="GtkStackPage">
124+
<property name="name">hybrid_qr</property>
125+
<property name="title">Scan the QR code to connect your device</property>
126+
<property name="child">
127+
<object class="GtkBox">
128+
<property name="orientation">vertical</property>
129+
<child>
130+
<object class="GtkSpinner">
131+
<binding name="visible">
132+
<lookup name="qr_spinner_visible">
133+
<lookup name="view-model">
134+
ExampleApplicationWindow
135+
</lookup>
136+
</lookup>
137+
</binding>
138+
</object>
139+
</child>
140+
<child>
141+
<object class="GtkPicture" id="qr_code_pic">
142+
<binding name="visible">
143+
<lookup name="qr_code_visible">
144+
<lookup name="view-model">
145+
ExampleApplicationWindow
146+
</lookup>
147+
</lookup>
148+
</binding>
149+
</object>
150+
</child>
151+
<child>
152+
<object class="GtkLabel">
153+
<binding name="label">
154+
<lookup name="prompt">
155+
<lookup name="view-model">
156+
ExampleApplicationWindow
157+
</lookup>
158+
</lookup>
159+
</binding>
160+
</object>
161+
</child>
162+
</object>
163+
</property>
164+
</object>
165+
</child>
166+
122167
<child>
123168
<object class="GtkStackPage">
124169
<property name="name">choose_credential</property>
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
use std::fmt::Debug;
2+
use std::task::Poll;
3+
4+
use async_std::channel::Receiver;
5+
use async_std::stream::Stream;
6+
use futures_lite::FutureExt;
7+
use libwebauthn::fido::{AuthenticatorData, AuthenticatorDataFlags};
8+
use libwebauthn::ops::webauthn::{Assertion, GetAssertionResponse};
9+
use libwebauthn::proto::ctap2::{Ctap2PublicKeyCredentialDescriptor, Ctap2Transport};
10+
use libwebauthn::transport::cable::qr_code_device::{CableQrCodeDevice, QrCodeOperationHint};
11+
use libwebauthn::transport::Device;
12+
use libwebauthn::webauthn::{Error as WebAuthnError, WebAuthn};
13+
14+
use crate::{
15+
dbus::CredentialRequest,
16+
tokio_runtime,
17+
};
18+
19+
use super::AuthenticatorResponse;
20+
21+
pub(crate) trait HybridHandler {
22+
type Stream: Stream<Item = HybridStateInternal>;
23+
fn start(&self, request: &CredentialRequest) -> Self::Stream;
24+
}
25+
26+
27+
#[derive(Debug)]
28+
pub struct InternalHybridHandler {}
29+
impl InternalHybridHandler {
30+
pub fn new() -> Self {
31+
Self { }
32+
}
33+
}
34+
35+
36+
impl HybridHandler for InternalHybridHandler {
37+
type Stream = InternalHybridStream;
38+
39+
fn start(&self, request: &CredentialRequest) -> Self::Stream {
40+
let request = request.clone();
41+
let (tx, rx) = async_std::channel::unbounded();
42+
async_std::task::spawn(async move {
43+
let hint = match request {
44+
CredentialRequest::CreatePublicKeyCredentialRequest(_) => QrCodeOperationHint::MakeCredential,
45+
CredentialRequest::GetPublicKeyCredentialRequest(_) => QrCodeOperationHint::GetAssertionRequest,
46+
};
47+
let mut device = CableQrCodeDevice::new_transient(hint);
48+
let qr_code = device.qr_code.to_string();
49+
if let Err(err) = tx.send(HybridStateInternal::Init(qr_code)).await {
50+
tracing::error!("Failed to send caBLE update: {:?}", err);
51+
return;
52+
};
53+
tokio_runtime::get().spawn(async move {
54+
let (mut channel, _) = device.channel().await.unwrap();
55+
let response: AuthenticatorResponse = loop {
56+
match &request {
57+
CredentialRequest::CreatePublicKeyCredentialRequest(make_request) => {
58+
match channel
59+
.webauthn_make_credential(&make_request)
60+
.await
61+
{
62+
Ok(response) => break Ok(response.into()),
63+
Err(WebAuthnError::Ctap(ctap_error)) => {
64+
if ctap_error.is_retryable_user_error() {
65+
tracing::debug!("Oops, try again! Error: {}", ctap_error);
66+
continue;
67+
}
68+
break Err(WebAuthnError::Ctap(ctap_error));
69+
}
70+
Err(err) => break Err(err),
71+
};
72+
73+
}
74+
CredentialRequest::GetPublicKeyCredentialRequest(get_request) => {
75+
match channel
76+
.webauthn_get_assertion(&get_request)
77+
.await
78+
{
79+
Ok(response) => break Ok(response.into()),
80+
Err(WebAuthnError::Ctap(ctap_error)) => {
81+
if ctap_error.is_retryable_user_error() {
82+
println!("Oops, try again! Error: {}", ctap_error);
83+
continue;
84+
}
85+
break Err(WebAuthnError::Ctap(ctap_error));
86+
}
87+
Err(err) => break Err(err),
88+
};
89+
90+
}
91+
}
92+
}
93+
.unwrap();
94+
if let Err(err) = tx.send(HybridStateInternal::Completed(response)).await {
95+
tracing::error!("Failed to send caBLE update: {:?}", err)
96+
}
97+
98+
});
99+
});
100+
InternalHybridStream { rx }
101+
}
102+
}
103+
104+
pub struct InternalHybridStream {
105+
rx: Receiver<HybridStateInternal>
106+
}
107+
108+
impl Stream for InternalHybridStream {
109+
type Item = HybridStateInternal;
110+
111+
fn poll_next(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Option<Self::Item>> {
112+
match self.rx.recv().poll(cx) {
113+
Poll::Pending => Poll::Pending,
114+
Poll::Ready(Ok(state)) => Poll::Ready(Some(state)),
115+
Poll::Ready(Err(_)) => Poll::Ready(None),
116+
}
117+
}
118+
}
119+
120+
#[derive(Debug)]
121+
pub struct DummyHybridHandler {
122+
stream: DummyHybridStateStream,
123+
}
124+
125+
impl DummyHybridHandler {
126+
#[cfg(test)]
127+
pub fn new(states: Vec<HybridStateInternal>) -> Self {
128+
Self {
129+
stream: DummyHybridStateStream { states }
130+
}
131+
}
132+
}
133+
134+
impl Default for DummyHybridHandler {
135+
fn default() -> Self {
136+
Self {
137+
stream: DummyHybridStateStream::default()
138+
}
139+
}
140+
}
141+
impl HybridHandler for DummyHybridHandler {
142+
type Stream = DummyHybridStateStream;
143+
144+
fn start(&self, _request: &CredentialRequest) -> Self::Stream {
145+
self.stream.clone()
146+
}
147+
}
148+
149+
#[derive(Clone, Debug)]
150+
pub struct DummyHybridStateStream {
151+
states: Vec<HybridStateInternal>,
152+
}
153+
154+
impl Default for DummyHybridStateStream {
155+
fn default() -> Self {
156+
let qr_code = String::from("FIDO:/078241338926040702789239694720083010994762289662861130514766991835876383562063181103169246410435938367110394959927031730060360967994421343201235185697538107096654083332");
157+
// SHA256("webauthn.io")
158+
let rp_id_hash = [
159+
0x74, 0xa6, 0xea, 0x92, 0x13, 0xc9, 0x9c, 0x2f, 0x74, 0xb2, 0x24, 0x92, 0xb3, 0x20,
160+
0xcf, 0x40, 0x26, 0x2a, 0x94, 0xc1, 0xa9, 0x50, 0xa0, 0x39, 0x7f, 0x29, 0x25, 0xb,
161+
0x60, 0x84, 0x1e, 0xf0,
162+
];
163+
164+
let auth_data = AuthenticatorData {
165+
rp_id_hash,
166+
flags: AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::USER_VERIFIED,
167+
signature_count: 1,
168+
attested_credential: None,
169+
extensions: None,
170+
};
171+
172+
let assertion = Assertion {
173+
credential_id: Some(Ctap2PublicKeyCredentialDescriptor {
174+
id: vec![0xca, 0xb1, 0xe].into(),
175+
r#type: libwebauthn::proto::ctap2::Ctap2PublicKeyCredentialType::PublicKey,
176+
transports: Some(vec![Ctap2Transport::Hybrid]),
177+
}),
178+
authenticator_data: auth_data,
179+
signature: Vec::new(),
180+
user: None,
181+
credentials_count: Some(1),
182+
user_selected: None,
183+
large_blob_key: None,
184+
unsigned_extensions_output: None,
185+
enterprise_attestation: None,
186+
attestation_statement: None,
187+
};
188+
let response = GetAssertionResponse {
189+
assertions: vec![assertion],
190+
};
191+
DummyHybridStateStream {
192+
states: vec![
193+
HybridStateInternal::Init(qr_code),
194+
HybridStateInternal::Waiting,
195+
HybridStateInternal::Connecting,
196+
HybridStateInternal::Completed(response.into()),
197+
],
198+
}
199+
}
200+
}
201+
202+
impl Stream for DummyHybridStateStream {
203+
type Item = HybridStateInternal;
204+
205+
fn poll_next(
206+
self: std::pin::Pin<&mut Self>,
207+
_cx: &mut std::task::Context<'_>,
208+
) -> Poll<Option<Self::Item>> {
209+
if self.states.len() == 0 {
210+
Poll::Ready(None)
211+
} else {
212+
Poll::Ready(Some((self.get_mut()).states.remove(0)))
213+
}
214+
}
215+
}
216+
217+
#[derive(Clone, Debug)]
218+
pub enum HybridStateInternal {
219+
/// The FIDO string to be displayed to the user, which contains QR secret
220+
/// and public key.
221+
Init(String),
222+
223+
/// Awaiting BLE advert from phone.
224+
Waiting,
225+
/// BLE advertisement has been received from phone, tunnel is being established
226+
Connecting,
227+
228+
/// Authenticator data
229+
Completed(AuthenticatorResponse),
230+
231+
// This isn't actually sent from the server.
232+
UserCancelled,
233+
}
234+
235+
#[derive(Clone, Debug)]
236+
pub enum HybridState {
237+
/// The FIDO string to be displayed to the user, which contains QR secret
238+
/// and public key.
239+
Init(String),
240+
241+
/// Awaiting BLE advert from phone.
242+
Waiting,
243+
/// BLE advertisement has been received from phone, tunnel is being established
244+
Connecting,
245+
246+
/// Authenticator data
247+
Completed,
248+
249+
// This isn't actually sent from the server.
250+
UserCancelled,
251+
}
252+
253+
impl From<HybridStateInternal> for HybridState {
254+
fn from(value: HybridStateInternal) -> Self {
255+
match value {
256+
HybridStateInternal::Init(qr_code) => HybridState::Init(qr_code),
257+
HybridStateInternal::Waiting => HybridState::Waiting,
258+
HybridStateInternal::Connecting => HybridState::Connecting,
259+
HybridStateInternal::Completed(_) => HybridState::Completed,
260+
HybridStateInternal::UserCancelled => HybridState::UserCancelled,
261+
262+
}
263+
}
264+
}

0 commit comments

Comments
 (0)