Skip to content

Commit 506cbf8

Browse files
Merge pull request #58 from msirringhaus/bio_management
Implement BioManagement
2 parents e918b1c + 12d6e0d commit 506cbf8

File tree

7 files changed

+926
-13
lines changed

7 files changed

+926
-13
lines changed
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
use std::error::Error;
2+
use std::fmt::Display;
3+
use std::io::{self, Write};
4+
use std::time::Duration;
5+
use text_io::read;
6+
use tracing_subscriber::{self, EnvFilter};
7+
8+
use libwebauthn::management::BioEnrollment;
9+
use libwebauthn::pin::{PinProvider, StdinPromptPinProvider};
10+
use libwebauthn::proto::ctap2::{Ctap2, Ctap2GetInfoResponse, Ctap2LastEnrollmentSampleStatus};
11+
use libwebauthn::transport::hid::list_devices;
12+
use libwebauthn::transport::Device;
13+
use libwebauthn::webauthn::Error as WebAuthnError;
14+
15+
const TIMEOUT: Duration = Duration::from_secs(10);
16+
17+
fn setup_logging() {
18+
tracing_subscriber::fmt()
19+
.with_env_filter(EnvFilter::from_default_env())
20+
.without_time()
21+
.init();
22+
}
23+
24+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25+
enum Operation {
26+
GetModality,
27+
GetFingerprintSensorInfo,
28+
EnumerateEnrollments,
29+
RemoveEnrollment,
30+
RenameEnrollment,
31+
AddNewEnrollment,
32+
}
33+
34+
impl Display for Operation {
35+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36+
match self {
37+
Operation::GetModality => f.write_str("Get modality"),
38+
Operation::GetFingerprintSensorInfo => f.write_str("Get fingerprint sensor info"),
39+
Operation::EnumerateEnrollments => f.write_str("Enumerate enrollments"),
40+
Operation::RemoveEnrollment => f.write_str("Remove enrollment"),
41+
Operation::RenameEnrollment => f.write_str("Rename enrollment"),
42+
Operation::AddNewEnrollment => f.write_str("Start new enrollment"),
43+
}
44+
}
45+
}
46+
47+
fn get_supported_options(info: &Ctap2GetInfoResponse) -> Vec<Operation> {
48+
let mut configure_ops = vec![];
49+
if let Some(options) = &info.options {
50+
if options.get("bioEnroll").is_some() {
51+
configure_ops.push(Operation::GetModality);
52+
configure_ops.push(Operation::GetFingerprintSensorInfo);
53+
if options.get("bioEnroll") == Some(&true) {
54+
configure_ops.push(Operation::EnumerateEnrollments);
55+
configure_ops.push(Operation::RemoveEnrollment);
56+
configure_ops.push(Operation::RenameEnrollment);
57+
}
58+
configure_ops.push(Operation::AddNewEnrollment);
59+
}
60+
}
61+
configure_ops
62+
}
63+
64+
fn ask_for_user_input(num_of_items: usize) -> usize {
65+
let idx = loop {
66+
print!("Your choice: ");
67+
io::stdout().flush().expect("Failed to flush stdout!");
68+
let input: String = read!("{}\n");
69+
if let Ok(idx) = input.trim().parse::<usize>() {
70+
if idx < num_of_items {
71+
println!();
72+
break idx;
73+
}
74+
}
75+
};
76+
idx
77+
}
78+
79+
fn print_status_update(enrollment_status: Ctap2LastEnrollmentSampleStatus, remaining_samples: u64) {
80+
use Ctap2LastEnrollmentSampleStatus as S;
81+
print!("Last sample status: ");
82+
match enrollment_status {
83+
S::Ctap2EnrollFeedbackFpGood => print!("Good"),
84+
S::Ctap2EnrollFeedbackFpTooHigh => print!("Fingerprint too high"),
85+
S::Ctap2EnrollFeedbackFpTooLow => print!("Fingerprint too low"),
86+
S::Ctap2EnrollFeedbackFpTooLeft => print!("Fingerprint too left"),
87+
S::Ctap2EnrollFeedbackFpTooRight => print!("Fingerprint too right"),
88+
S::Ctap2EnrollFeedbackFpTooFast => print!("Fingerprint too fast"),
89+
S::Ctap2EnrollFeedbackFpTooSlow => print!("Fingerprint too slow"),
90+
S::Ctap2EnrollFeedbackFpPoorQuality => print!("Fingerprint poor quality"),
91+
S::Ctap2EnrollFeedbackFpTooSkewed => print!("Fingerprint too skewed"),
92+
S::Ctap2EnrollFeedbackFpTooShort => print!("Fingerprint too short"),
93+
S::Ctap2EnrollFeedbackFpMergeFailure => print!("Fingerprint merge failure"),
94+
S::Ctap2EnrollFeedbackFpExists => print!("Fingerprint exists"),
95+
S::Unused => print!("<Unused>"),
96+
S::Ctap2EnrollFeedbackNoUserActivity => print!("No user activity"),
97+
S::Ctap2EnrollFeedbackNoUserPresenceTransition => print!("No user presence transition"),
98+
}
99+
println!(", Remaining samples needed: {remaining_samples}");
100+
}
101+
102+
#[tokio::main]
103+
pub async fn main() -> Result<(), Box<dyn Error>> {
104+
setup_logging();
105+
106+
let devices = list_devices().await.unwrap();
107+
println!("Devices found: {:?}", devices);
108+
let mut pin_provider: Box<dyn PinProvider> = Box::new(StdinPromptPinProvider::new());
109+
110+
for mut device in devices {
111+
println!("Selected HID authenticator: {}", &device);
112+
device.wink(TIMEOUT).await?;
113+
114+
let mut channel = device.channel().await?;
115+
let info = channel.ctap2_get_info().await?;
116+
let options = get_supported_options(&info);
117+
118+
println!("What do you want to do?");
119+
println!();
120+
for (idx, op) in options.iter().enumerate() {
121+
println!("({idx}) {op}");
122+
}
123+
124+
let idx = ask_for_user_input(options.len());
125+
let resp = 'outer: loop {
126+
let action = match options[idx] {
127+
Operation::GetModality => channel
128+
.get_bio_modality(TIMEOUT)
129+
.await
130+
.map(|x| format!("{x:?}")),
131+
Operation::GetFingerprintSensorInfo => channel
132+
.get_fingerprint_sensor_info(TIMEOUT)
133+
.await
134+
.map(|x| format!("{x:?}")),
135+
Operation::EnumerateEnrollments => channel
136+
.get_bio_enrollments(&mut pin_provider, TIMEOUT)
137+
.await
138+
.map(|x| format!("{x:?}")),
139+
Operation::RemoveEnrollment => {
140+
let enrollments = loop {
141+
match channel
142+
.get_bio_enrollments(&mut pin_provider, TIMEOUT)
143+
.await
144+
{
145+
Ok(r) => break r,
146+
Err(WebAuthnError::Ctap(ctap_error)) => {
147+
if ctap_error.is_retryable_user_error() {
148+
println!("Oops, try again! Error: {}", ctap_error);
149+
continue;
150+
}
151+
break 'outer Err(WebAuthnError::Ctap(ctap_error));
152+
}
153+
Err(err) => break 'outer Err(err),
154+
}
155+
};
156+
println!("Which enrollment do you want to remove?");
157+
for (id, enrollment) in enrollments.iter().enumerate() {
158+
println!("{id} {enrollment:?}")
159+
}
160+
let idx = ask_for_user_input(enrollments.len());
161+
channel
162+
.remove_bio_enrollment(
163+
&enrollments[idx].template_id.as_ref().unwrap(),
164+
&mut pin_provider,
165+
TIMEOUT,
166+
)
167+
.await
168+
.map(|x| format!("{x:?}"))
169+
}
170+
Operation::RenameEnrollment => {
171+
let enrollments = loop {
172+
match channel
173+
.get_bio_enrollments(&mut pin_provider, TIMEOUT)
174+
.await
175+
{
176+
Ok(r) => break r,
177+
Err(WebAuthnError::Ctap(ctap_error)) => {
178+
if ctap_error.is_retryable_user_error() {
179+
println!("Oops, try again! Error: {}", ctap_error);
180+
continue;
181+
}
182+
break 'outer Err(WebAuthnError::Ctap(ctap_error));
183+
}
184+
Err(err) => break 'outer Err(err),
185+
}
186+
};
187+
println!("Which enrollment do you want to rename?");
188+
for (id, enrollment) in enrollments.iter().enumerate() {
189+
println!("{id} {enrollment:?}")
190+
}
191+
let idx = ask_for_user_input(enrollments.len());
192+
print!("New name: ");
193+
io::stdout().flush().expect("Failed to flush stdout!");
194+
let new_name: String = read!("{}\n");
195+
channel
196+
.rename_bio_enrollment(
197+
&enrollments[idx].template_id.as_ref().unwrap(),
198+
&new_name,
199+
&mut pin_provider,
200+
TIMEOUT,
201+
)
202+
.await
203+
.map(|x| format!("{x:?}"))
204+
}
205+
Operation::AddNewEnrollment => {
206+
let (template_id, mut sample_status, mut remaining_samples) = match channel
207+
.start_new_bio_enrollment(&mut pin_provider, None, TIMEOUT)
208+
.await
209+
{
210+
Ok(r) => r,
211+
Err(WebAuthnError::Ctap(ctap_error)) => {
212+
if ctap_error.is_retryable_user_error() {
213+
println!("Oops, try again! Error: {}", ctap_error);
214+
continue;
215+
}
216+
break Err(WebAuthnError::Ctap(ctap_error));
217+
}
218+
Err(err) => break Err(err),
219+
};
220+
while remaining_samples > 0 {
221+
print_status_update(sample_status, remaining_samples);
222+
(sample_status, remaining_samples) = match channel
223+
.capture_next_bio_enrollment_sample(
224+
&template_id,
225+
&mut pin_provider,
226+
None,
227+
TIMEOUT,
228+
)
229+
.await
230+
{
231+
Ok(r) => r,
232+
Err(WebAuthnError::Ctap(ctap_error)) => {
233+
if ctap_error.is_retryable_user_error() {
234+
println!("Oops, try again! Error: {}", ctap_error);
235+
continue;
236+
}
237+
break 'outer Err(WebAuthnError::Ctap(ctap_error));
238+
}
239+
Err(err) => break 'outer Err(err),
240+
};
241+
}
242+
Ok(format!("Success!"))
243+
}
244+
};
245+
match action {
246+
Ok(r) => break Ok(r),
247+
Err(WebAuthnError::Ctap(ctap_error)) => {
248+
if ctap_error.is_retryable_user_error() {
249+
println!("Oops, try again! Error: {}", ctap_error);
250+
continue;
251+
}
252+
break Err(WebAuthnError::Ctap(ctap_error));
253+
}
254+
Err(err) => break Err(err),
255+
};
256+
}
257+
.unwrap();
258+
println!("Bio enrollment command done: {resp}");
259+
}
260+
261+
Ok(())
262+
}

0 commit comments

Comments
 (0)