Skip to content

Commit 5f0e51e

Browse files
authored
Implement Credential Management (#67)
Fixes #24
1 parent daa0f62 commit 5f0e51e

File tree

9 files changed

+879
-10
lines changed

9 files changed

+879
-10
lines changed
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
use libwebauthn::management::CredentialManagement;
2+
use libwebauthn::pin::{PinProvider, StdinPromptPinProvider};
3+
use libwebauthn::proto::ctap2::{
4+
Ctap2, Ctap2CredentialData, Ctap2PublicKeyCredentialRpEntity, Ctap2RPData,
5+
};
6+
use libwebauthn::proto::CtapError;
7+
use libwebauthn::transport::hid::list_devices;
8+
use libwebauthn::transport::Device;
9+
use libwebauthn::webauthn::Error as WebAuthnError;
10+
use std::fmt::Display;
11+
use std::io::{self, Write};
12+
use std::time::Duration;
13+
use text_io::read;
14+
use tracing_subscriber::{self, EnvFilter};
15+
16+
const TIMEOUT: Duration = Duration::from_secs(10);
17+
18+
fn setup_logging() {
19+
tracing_subscriber::fmt()
20+
.with_env_filter(EnvFilter::from_default_env())
21+
.without_time()
22+
.init();
23+
}
24+
25+
macro_rules! handle_retries {
26+
($res:expr) => {
27+
loop {
28+
match $res.await {
29+
Ok(r) => break r,
30+
Err(WebAuthnError::Ctap(ctap_error)) => {
31+
if ctap_error.is_retryable_user_error() {
32+
println!("Oops, try again! Error: {}", ctap_error);
33+
continue;
34+
}
35+
return Err(WebAuthnError::Ctap(ctap_error));
36+
}
37+
Err(err) => return Err(err),
38+
}
39+
}
40+
};
41+
}
42+
43+
fn format_rp(rp: &Ctap2PublicKeyCredentialRpEntity) -> String {
44+
rp.name.clone().unwrap_or(rp.id.clone())
45+
}
46+
47+
fn format_credential(cred: &Ctap2CredentialData) -> String {
48+
cred.user
49+
.display_name
50+
.clone()
51+
.unwrap_or(cred.user.name.clone().unwrap_or("<No username>".into()))
52+
.to_string()
53+
}
54+
55+
async fn enumerate_rps<T: CredentialManagement>(
56+
channel: &mut T,
57+
pin_provider: &mut Box<dyn PinProvider>,
58+
) -> Result<Vec<Ctap2RPData>, WebAuthnError> {
59+
let (rp, total_rps) = handle_retries!(channel.enumerate_rps_begin(pin_provider, TIMEOUT));
60+
let mut rps = vec![rp];
61+
// Starting at 1, as we already have one from the begin-call.
62+
for _ in 1..total_rps {
63+
let rp = handle_retries!(channel.enumerate_rps_next_rp(pin_provider, TIMEOUT));
64+
rps.push(rp);
65+
}
66+
Ok(rps)
67+
}
68+
69+
async fn enumerate_credentials_for_rp<T: CredentialManagement>(
70+
channel: &mut T,
71+
pin_provider: &mut Box<dyn PinProvider>,
72+
rp_id_hash: &[u8],
73+
) -> Result<Vec<Ctap2CredentialData>, WebAuthnError> {
74+
let (credential, num_of_creds) =
75+
handle_retries!(channel.enumerate_credentials_begin(pin_provider, rp_id_hash, TIMEOUT));
76+
let mut credentials = vec![credential];
77+
// Starting at 1, as we already have one from the begin-call.
78+
for _ in 1..num_of_creds {
79+
let credential = handle_retries!(channel.enumerate_credentials_next(pin_provider, TIMEOUT));
80+
credentials.push(credential);
81+
}
82+
Ok(credentials)
83+
}
84+
85+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
86+
enum Operation {
87+
GetMetadata,
88+
EnumerateRPs,
89+
EnumerateCredentials,
90+
RemoveCredential,
91+
UpdateUserInfo,
92+
}
93+
94+
impl Display for Operation {
95+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96+
match self {
97+
Operation::GetMetadata => f.write_str("Get metadata"),
98+
Operation::EnumerateRPs => f.write_str("Enumerate relying parties"),
99+
Operation::EnumerateCredentials => f.write_str("Enumerate credentials"),
100+
Operation::RemoveCredential => f.write_str("Remove credential"),
101+
Operation::UpdateUserInfo => f.write_str("Update user info"),
102+
}
103+
}
104+
}
105+
106+
fn ask_for_user_input(num_of_items: usize) -> usize {
107+
let idx = loop {
108+
print!("Your choice: ");
109+
io::stdout().flush().expect("Failed to flush stdout!");
110+
let input: String = read!("{}\n");
111+
if let Ok(idx) = input.trim().parse::<usize>() {
112+
if idx < num_of_items {
113+
println!();
114+
break idx;
115+
}
116+
}
117+
};
118+
idx
119+
}
120+
121+
#[tokio::main]
122+
pub async fn main() -> Result<(), WebAuthnError> {
123+
setup_logging();
124+
125+
let devices = list_devices().await.unwrap();
126+
println!("Devices found: {:?}", devices);
127+
let mut pin_provider: Box<dyn PinProvider> = Box::new(StdinPromptPinProvider::new());
128+
129+
for mut device in devices {
130+
println!("Selected HID authenticator: {}", &device);
131+
device.wink(TIMEOUT).await?;
132+
133+
let mut channel = device.channel().await?;
134+
let info = channel.ctap2_get_info().await?;
135+
136+
if let Some(options) = &info.options {
137+
if options.get("credMgmt") != Some(&true) {
138+
println!("Your token does not support credential management.");
139+
return Err(WebAuthnError::Ctap(CtapError::InvalidCommand));
140+
}
141+
}
142+
143+
let options = [
144+
Operation::GetMetadata,
145+
Operation::EnumerateRPs,
146+
Operation::EnumerateCredentials,
147+
Operation::RemoveCredential,
148+
Operation::UpdateUserInfo,
149+
];
150+
151+
println!("What do you want to do?");
152+
println!();
153+
for (idx, op) in options.iter().enumerate() {
154+
println!("({idx}) {op}");
155+
}
156+
157+
let idx = ask_for_user_input(options.len());
158+
let metadata = handle_retries!(channel.get_credential_metadata(&mut pin_provider, TIMEOUT));
159+
if options[idx] == Operation::GetMetadata {
160+
println!("Metadata: {metadata:#?}");
161+
return Ok(());
162+
}
163+
164+
let rps = enumerate_rps(&mut channel, &mut pin_provider).await?;
165+
if options[idx] == Operation::EnumerateRPs {
166+
println!("RPs:");
167+
for rp in &rps {
168+
println!("{}", format_rp(&rp.rp));
169+
}
170+
return Ok(());
171+
}
172+
173+
let mut credlist = Vec::new();
174+
for rp in &rps {
175+
let creds =
176+
enumerate_credentials_for_rp(&mut channel, &mut pin_provider, &rp.rp_id_hash)
177+
.await?;
178+
for cred in creds {
179+
credlist.push((rp.rp.clone(), cred));
180+
}
181+
}
182+
if options[idx] == Operation::EnumerateCredentials {
183+
println!("Credentials:");
184+
for (rp, cred) in &credlist {
185+
println!("{}: {}", format_rp(rp), format_credential(cred));
186+
}
187+
return Ok(());
188+
}
189+
190+
// For all remaining operations, we need to enumerate the found creds
191+
for (idx, (rp, cred)) in credlist.iter().enumerate() {
192+
println!("({idx}) {}: {}", format_rp(rp), format_credential(cred));
193+
}
194+
195+
let cred_idx = ask_for_user_input(options.len());
196+
197+
if options[idx] == Operation::RemoveCredential {
198+
let (_, cred) = &credlist[cred_idx];
199+
handle_retries!(channel.delete_credential(
200+
&cred.credential_id,
201+
&mut pin_provider,
202+
TIMEOUT
203+
));
204+
println!("Done");
205+
return Ok(());
206+
}
207+
208+
if options[idx] == Operation::UpdateUserInfo {
209+
let name = loop {
210+
print!("New user name: ");
211+
io::stdout().flush().expect("Failed to flush stdout!");
212+
let input: String = read!("{}\n");
213+
let input = input.trim();
214+
if !input.is_empty() {
215+
println!();
216+
break input.to_string();
217+
}
218+
};
219+
let (_rp, cred) = &credlist[cred_idx];
220+
let mut user = cred.user.clone();
221+
user.name = Some(name);
222+
handle_retries!(channel.update_user_info(
223+
&cred.credential_id,
224+
&user,
225+
&mut pin_provider,
226+
TIMEOUT
227+
));
228+
println!("Done");
229+
return Ok(());
230+
}
231+
}
232+
233+
Ok(())
234+
}

libwebauthn/src/management.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ pub use bio_enrollment::BioEnrollment;
33

44
mod authenticator_config;
55
pub use authenticator_config::AuthenticatorConfig;
6+
7+
mod credential_management;
8+
pub use credential_management::CredentialManagement;

0 commit comments

Comments
 (0)