diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /third_party/rust/authenticator/examples | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/authenticator/examples')
6 files changed, 2063 insertions, 0 deletions
diff --git a/third_party/rust/authenticator/examples/ctap2.rs b/third_party/rust/authenticator/examples/ctap2.rs new file mode 100644 index 0000000000..be742a5e0e --- /dev/null +++ b/third_party/rust/authenticator/examples/ctap2.rs @@ -0,0 +1,285 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use authenticator::{ + authenticatorservice::{AuthenticatorService, RegisterArgs, SignArgs}, + crypto::COSEAlgorithm, + ctap2::server::{ + AuthenticationExtensionsClientInputs, CredentialProtectionPolicy, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, + PublicKeyCredentialUserEntity, RelyingParty, ResidentKeyRequirement, Transport, + UserVerificationRequirement, + }, + statecallback::StateCallback, + Pin, StatusPinUv, StatusUpdate, +}; +use getopts::Options; +use rand::{thread_rng, RngCore}; +use std::sync::mpsc::{channel, RecvError}; +use std::{env, thread}; + +fn print_usage(program: &str, opts: Options) { + let brief = format!("Usage: {program} [options]"); + print!("{}", opts.usage(&brief)); +} + +fn main() { + env_logger::init(); + + let args: Vec<String> = env::args().collect(); + let program = args[0].clone(); + + let rp_id = "example.com".to_string(); + let app_id = "https://fido.example.com/myApp".to_string(); + + let mut opts = Options::new(); + opts.optflag("h", "help", "print this help menu").optopt( + "t", + "timeout", + "timeout in seconds", + "SEC", + ); + opts.optflag( + "a", + "app_id", + &format!("Using App ID {app_id} from origin 'https://{rp_id}'"), + ); + opts.optflag("s", "hmac_secret", "With hmac-secret"); + opts.optflag("h", "help", "print this help menu"); + opts.optflag("f", "fallback", "Use CTAP1 fallback implementation"); + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(f) => panic!("{}", f.to_string()), + }; + if matches.opt_present("help") { + print_usage(&program, opts); + return; + } + + let using_app_id = matches.opt_present("app_id"); + + let mut manager = + AuthenticatorService::new().expect("The auth service should initialize safely"); + manager.add_u2f_usb_hid_platform_transports(); + + let fallback = matches.opt_present("fallback"); + + let timeout_ms = match matches.opt_get_default::<u64>("timeout", 25) { + Ok(timeout_s) => { + println!("Using {}s as the timeout", &timeout_s); + timeout_s * 1_000 + } + Err(e) => { + println!("{e}"); + print_usage(&program, opts); + return; + } + }; + + println!("Asking a security key to register now..."); + let mut chall_bytes = [0u8; 32]; + thread_rng().fill_bytes(&mut chall_bytes); + + let (status_tx, status_rx) = channel::<StatusUpdate>(); + thread::spawn(move || loop { + match status_rx.recv() { + Ok(StatusUpdate::InteractiveManagement(..)) => { + panic!("STATUS: This can't happen when doing non-interactive usage"); + } + Ok(StatusUpdate::SelectDeviceNotice) => { + println!("STATUS: Please select a device by touching one of them."); + } + Ok(StatusUpdate::PresenceRequired) => { + println!("STATUS: waiting for user presence"); + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => { + let raw_pin = + rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN"); + sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN"); + continue; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => { + println!( + "Wrong PIN! {}", + attempts.map_or("Try again.".to_string(), |a| format!( + "You have {a} attempts left." + )) + ); + let raw_pin = + rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN"); + sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN"); + continue; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => { + panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.") + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => { + panic!("Too many failed attempts. Your device has been blocked. Reset it.") + } + Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => { + println!( + "Wrong UV! {}", + attempts.map_or("Try again.".to_string(), |a| format!( + "You have {a} attempts left." + )) + ); + continue; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => { + println!("Too many failed UV-attempts."); + continue; + } + Ok(StatusUpdate::PinUvError(e)) => { + panic!("Unexpected error: {:?}", e) + } + Ok(StatusUpdate::SelectResultNotice(_, _)) => { + panic!("Unexpected select device notice") + } + Err(RecvError) => { + println!("STATUS: end"); + return; + } + } + }); + + let user = PublicKeyCredentialUserEntity { + id: "user_id".as_bytes().to_vec(), + name: Some("A. User".to_string()), + display_name: None, + }; + // If we're testing AppID support, then register with an RP ID that isn't valid for the origin. + let relying_party = RelyingParty { + id: if using_app_id { + app_id.clone() + } else { + rp_id.clone() + }, + name: None, + }; + let ctap_args = RegisterArgs { + client_data_hash: chall_bytes, + relying_party, + origin: format!("https://{rp_id}"), + user, + pub_cred_params: vec![ + PublicKeyCredentialParameters { + alg: COSEAlgorithm::ES256, + }, + PublicKeyCredentialParameters { + alg: COSEAlgorithm::RS256, + }, + ], + exclude_list: vec![PublicKeyCredentialDescriptor { + id: vec![ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, + 0x1c, 0x1d, 0x1e, 0x1f, + ], + transports: vec![Transport::USB, Transport::NFC], + }], + user_verification_req: UserVerificationRequirement::Preferred, + resident_key_req: ResidentKeyRequirement::Discouraged, + extensions: AuthenticationExtensionsClientInputs { + cred_props: Some(true), + hmac_create_secret: Some(matches.opt_present("hmac_secret")), + min_pin_length: Some(true), + credential_protection_policy: Some( + CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList, + ), + ..Default::default() + }, + pin: None, + use_ctap1_fallback: fallback, + }; + + let attestation_object; + loop { + let (register_tx, register_rx) = channel(); + let callback = StateCallback::new(Box::new(move |rv| { + register_tx.send(rv).unwrap(); + })); + + if let Err(e) = manager.register(timeout_ms, ctap_args, status_tx.clone(), callback) { + panic!("Couldn't register: {:?}", e); + }; + + let register_result = register_rx + .recv() + .expect("Problem receiving, unable to continue"); + match register_result { + Ok(a) => { + println!("Ok!"); + attestation_object = a; + break; + } + Err(e) => panic!("Registration failed: {:?}", e), + }; + } + + println!("Register result: {:?}", &attestation_object); + + println!(); + println!("*********************************************************************"); + println!("Asking a security key to sign now, with the data from the register..."); + println!("*********************************************************************"); + + let allow_list; + if let Some(cred_data) = attestation_object.att_obj.auth_data.credential_data { + allow_list = vec![PublicKeyCredentialDescriptor { + id: cred_data.credential_id, + transports: vec![Transport::USB], + }]; + } else { + allow_list = Vec::new(); + } + + let ctap_args = SignArgs { + client_data_hash: chall_bytes, + origin: format!("https://{rp_id}"), + relying_party_id: rp_id, + allow_list, + user_verification_req: UserVerificationRequirement::Preferred, + user_presence_req: true, + extensions: AuthenticationExtensionsClientInputs { + app_id: using_app_id.then(|| app_id.clone()), + ..Default::default() + }, + pin: None, + use_ctap1_fallback: fallback, + }; + + loop { + let (sign_tx, sign_rx) = channel(); + + let callback = StateCallback::new(Box::new(move |rv| { + sign_tx.send(rv).unwrap(); + })); + + if let Err(e) = manager.sign(timeout_ms, ctap_args, status_tx, callback) { + panic!("Couldn't sign: {:?}", e); + } + + let sign_result = sign_rx + .recv() + .expect("Problem receiving, unable to continue"); + + match sign_result { + Ok(assertion_object) => { + println!("Assertion Object: {assertion_object:?}"); + if using_app_id { + println!( + "Used AppID: {}", + assertion_object + .extensions + .app_id + .expect("app id extension should be set") + ); + } + println!("Done."); + break; + } + Err(e) => panic!("Signing failed: {:?}", e), + } + } +} diff --git a/third_party/rust/authenticator/examples/ctap2_discoverable_creds.rs b/third_party/rust/authenticator/examples/ctap2_discoverable_creds.rs new file mode 100644 index 0000000000..d19ccc6f9e --- /dev/null +++ b/third_party/rust/authenticator/examples/ctap2_discoverable_creds.rs @@ -0,0 +1,380 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use authenticator::{ + authenticatorservice::{AuthenticatorService, RegisterArgs, SignArgs}, + crypto::COSEAlgorithm, + ctap2::server::{ + AuthenticationExtensionsClientInputs, PublicKeyCredentialDescriptor, + PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, RelyingParty, + ResidentKeyRequirement, Transport, UserVerificationRequirement, + }, + statecallback::StateCallback, + Pin, StatusPinUv, StatusUpdate, +}; +use getopts::Options; +use sha2::{Digest, Sha256}; +use std::sync::mpsc::{channel, RecvError}; +use std::{env, io, thread}; +use std::io::Write; + +fn print_usage(program: &str, opts: Options) { + println!("------------------------------------------------------------------------"); + println!("This program registers 3x the same origin with different users and"); + println!("requests 'discoverable credentials' for them."); + println!("After that, we try to log in to that origin and list all credentials found."); + println!("------------------------------------------------------------------------"); + let brief = format!("Usage: {program} [options]"); + print!("{}", opts.usage(&brief)); +} + +fn ask_user_choice(choices: &[PublicKeyCredentialUserEntity]) -> Option<usize> { + for (idx, op) in choices.iter().enumerate() { + println!("({idx}) \"{}\"", op.name.as_ref().unwrap()); + } + println!("({}) Cancel", choices.len()); + + let mut input = String::new(); + loop { + input.clear(); + print!("Your choice: "); + io::stdout() + .lock() + .flush() + .expect("Failed to flush stdout!"); + io::stdin() + .read_line(&mut input) + .expect("error: unable to read user input"); + if let Ok(idx) = input.trim().parse::<usize>() { + if idx < choices.len() { + // Add a newline in case of success for better separation of in/output + println!(); + return Some(idx); + } else if idx == choices.len() { + println!(); + return None; + } + println!("invalid input"); + } + } +} + +fn register_user(manager: &mut AuthenticatorService, username: &str, timeout_ms: u64) { + println!(); + println!("*********************************************************************"); + println!("Asking a security key to register now with user: {username}"); + println!("*********************************************************************"); + + println!("Asking a security key to register now..."); + let challenge_str = format!( + "{}{}{}{}", + r#"{"challenge": "1vQ9mxionq0ngCnjD-wTsv1zUSrGRtFqG2xP09SbZ70","#, + r#" "version": "U2F_V2", "appId": "http://example.com", "username": ""#, + username, + r#""}"# + ); + let mut challenge = Sha256::new(); + challenge.update(challenge_str.as_bytes()); + let chall_bytes = challenge.finalize().into(); + + let (status_tx, status_rx) = channel::<StatusUpdate>(); + thread::spawn(move || loop { + match status_rx.recv() { + Ok(StatusUpdate::InteractiveManagement(..)) => { + panic!("STATUS: This can't happen when doing non-interactive usage"); + } + Ok(StatusUpdate::SelectDeviceNotice) => { + println!("STATUS: Please select a device by touching one of them."); + } + Ok(StatusUpdate::PresenceRequired) => { + println!("STATUS: waiting for user presence"); + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => { + let raw_pin = + rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN"); + sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN"); + continue; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => { + println!( + "Wrong PIN! {}", + attempts.map_or("Try again.".to_string(), |a| format!( + "You have {a} attempts left." + )) + ); + let raw_pin = + rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN"); + sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN"); + continue; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => { + panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.") + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => { + panic!("Too many failed attempts. Your device has been blocked. Reset it.") + } + Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => { + println!( + "Wrong UV! {}", + attempts.map_or("Try again.".to_string(), |a| format!( + "You have {a} attempts left." + )) + ); + continue; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => { + println!("Too many failed UV-attempts."); + continue; + } + Ok(StatusUpdate::PinUvError(e)) => { + panic!("Unexpected error: {:?}", e) + } + Ok(StatusUpdate::SelectResultNotice(_, _)) => { + panic!("Unexpected select result notice") + } + Err(RecvError) => { + println!("STATUS: end"); + return; + } + } + }); + + let user = PublicKeyCredentialUserEntity { + id: username.as_bytes().to_vec(), + name: Some(username.to_string()), + display_name: None, + }; + let origin = "https://example.com".to_string(); + let ctap_args = RegisterArgs { + client_data_hash: chall_bytes, + relying_party: RelyingParty { + id: "example.com".to_string(), + name: None, + }, + origin, + user, + pub_cred_params: vec![ + PublicKeyCredentialParameters { + alg: COSEAlgorithm::ES256, + }, + PublicKeyCredentialParameters { + alg: COSEAlgorithm::RS256, + }, + ], + exclude_list: vec![PublicKeyCredentialDescriptor { + id: vec![], + transports: vec![Transport::USB, Transport::NFC], + }], + user_verification_req: UserVerificationRequirement::Required, + resident_key_req: ResidentKeyRequirement::Required, + extensions: AuthenticationExtensionsClientInputs { + cred_props: Some(true), + ..Default::default() + }, + pin: None, + use_ctap1_fallback: false, + }; + + let attestation_object; + loop { + let (register_tx, register_rx) = channel(); + let callback = StateCallback::new(Box::new(move |rv| { + register_tx.send(rv).unwrap(); + })); + + if let Err(e) = manager.register(timeout_ms, ctap_args, status_tx, callback) { + panic!("Couldn't register: {:?}", e); + }; + + let register_result = register_rx + .recv() + .expect("Problem receiving, unable to continue"); + match register_result { + Ok(a) => { + println!("Ok!"); + attestation_object = a; + break; + } + Err(e) => panic!("Registration failed: {:?}", e), + }; + } + + println!("Register result: {:?}", &attestation_object); +} + +fn main() { + env_logger::init(); + + let args: Vec<String> = env::args().collect(); + let program = args[0].clone(); + + let mut opts = Options::new(); + opts.optflag("h", "help", "print this help menu").optopt( + "t", + "timeout", + "timeout in seconds", + "SEC", + ); + opts.optflag( + "s", + "skip_reg", + "Skip registration"); + + opts.optflag("h", "help", "print this help menu"); + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(f) => panic!("{}", f.to_string()), + }; + if matches.opt_present("help") { + print_usage(&program, opts); + return; + } + + let mut manager = + AuthenticatorService::new().expect("The auth service should initialize safely"); + manager.add_u2f_usb_hid_platform_transports(); + + let timeout_ms = match matches.opt_get_default::<u64>("timeout", 15) { + Ok(timeout_s) => { + println!("Using {}s as the timeout", &timeout_s); + timeout_s * 1_000 + } + Err(e) => { + println!("{e}"); + print_usage(&program, opts); + return; + } + }; + + if !matches.opt_present("skip_reg") { + for username in &["A. User", "A. Nother", "Dr. Who"] { + register_user(&mut manager, username, timeout_ms) + } + } + + println!(); + println!("*********************************************************************"); + println!("Asking a security key to sign now, with the data from the register..."); + println!("*********************************************************************"); + + // Discovering creds: + let allow_list = Vec::new(); + let origin = "https://example.com".to_string(); + let challenge_str = format!( + "{}{}", + r#"{"challenge": "1vQ9mxionq0ngCnjD-wTsv1zUSrGRtFqG2xP09SbZ70","#, + r#" "version": "U2F_V2", "appId": "http://example.com" "#, + ); + + let (status_tx, status_rx) = channel::<StatusUpdate>(); + thread::spawn(move || loop { + match status_rx.recv() { + Ok(StatusUpdate::InteractiveManagement(..)) => { + panic!("STATUS: This can't happen when doing non-interactive usage"); + } + Ok(StatusUpdate::SelectDeviceNotice) => { + println!("STATUS: Please select a device by touching one of them."); + } + Ok(StatusUpdate::PresenceRequired) => { + println!("STATUS: waiting for user presence"); + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => { + let raw_pin = + rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN"); + sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN"); + continue; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => { + println!( + "Wrong PIN! {}", + attempts.map_or("Try again.".to_string(), |a| format!( + "You have {a} attempts left." + )) + ); + let raw_pin = + rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN"); + sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN"); + continue; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => { + panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.") + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => { + panic!("Too many failed attempts. Your device has been blocked. Reset it.") + } + Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => { + println!( + "Wrong UV! {}", + attempts.map_or("Try again.".to_string(), |a| format!( + "You have {a} attempts left." + )) + ); + continue; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => { + println!("Too many failed UV-attempts."); + continue; + } + Ok(StatusUpdate::PinUvError(e)) => { + panic!("Unexpected error: {:?}", e) + } + Ok(StatusUpdate::SelectResultNotice(index_sender, users)) => { + println!("Multiple signatures returned. Select one or cancel."); + let idx = ask_user_choice(&users); + index_sender.send(idx).expect("Failed to send choice"); + } + Err(RecvError) => { + println!("STATUS: end"); + return; + } + } + }); + + let mut challenge = Sha256::new(); + challenge.update(challenge_str.as_bytes()); + let chall_bytes = challenge.finalize().into(); + let ctap_args = SignArgs { + client_data_hash: chall_bytes, + origin, + relying_party_id: "example.com".to_string(), + allow_list, + user_verification_req: UserVerificationRequirement::Required, + user_presence_req: true, + extensions: Default::default(), + pin: None, + use_ctap1_fallback: false, + }; + + loop { + let (sign_tx, sign_rx) = channel(); + + let callback = StateCallback::new(Box::new(move |rv| { + sign_tx.send(rv).unwrap(); + })); + + if let Err(e) = manager.sign(timeout_ms, ctap_args, status_tx, callback) { + panic!("Couldn't sign: {:?}", e); + } + + let sign_result = sign_rx + .recv() + .expect("Problem receiving, unable to continue"); + + match sign_result { + Ok(assertion_object) => { + println!("Assertion Object: {assertion_object:?}"); + println!("-----------------------------------------------------------------"); + println!("Found credentials:"); + println!( + "{:?}", + assertion_object.assertion.user.clone().unwrap().name.unwrap() // Unwrapping here, as these shouldn't fail + ); + println!("-----------------------------------------------------------------"); + println!("Done."); + break; + } + Err(e) => panic!("Signing failed: {:?}", e), + } + } +} diff --git a/third_party/rust/authenticator/examples/interactive_management.rs b/third_party/rust/authenticator/examples/interactive_management.rs new file mode 100644 index 0000000000..6ef0e9dcc3 --- /dev/null +++ b/third_party/rust/authenticator/examples/interactive_management.rs @@ -0,0 +1,810 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use authenticator::{ + authenticatorservice::AuthenticatorService, + ctap2::commands::{ + authenticator_config::{AuthConfigCommand, AuthConfigResult, SetMinPINLength}, + bio_enrollment::EnrollmentInfo, + credential_management::CredentialList, + PinUvAuthResult, + }, + errors::AuthenticatorError, + statecallback::StateCallback, + AuthenticatorInfo, BioEnrollmentCmd, BioEnrollmentResult, CredManagementCmd, + CredentialManagementResult, InteractiveRequest, InteractiveUpdate, ManageResult, Pin, + StatusPinUv, StatusUpdate, +}; +use getopts::Options; +use log::debug; +use std::{env, io, sync::mpsc::Sender, thread}; +use std::{ + fmt::Display, + io::Write, + sync::mpsc::{channel, Receiver, RecvError}, +}; + +#[derive(Debug, PartialEq, Clone)] +enum PinOperation { + Set, + Change, +} + +#[derive(Debug, Clone, PartialEq)] +enum ConfigureOperation { + ToggleAlwaysUV, + EnableEnterpriseAttestation, + SetMinPINLength, +} + +impl ConfigureOperation { + fn ask_user(info: &AuthenticatorInfo) -> Self { + let sub_level = Self::parse_possible_operations(info); + println!(); + println!("What do you wish to do?"); + let choice = ask_user_choice(&sub_level); + sub_level[choice].clone() + } + + fn parse_possible_operations(info: &AuthenticatorInfo) -> Vec<Self> { + let mut configure_ops = vec![]; + if info.options.authnr_cfg == Some(true) && info.options.always_uv.is_some() { + configure_ops.push(ConfigureOperation::ToggleAlwaysUV); + } + if info.options.authnr_cfg == Some(true) && info.options.set_min_pin_length.is_some() { + configure_ops.push(ConfigureOperation::SetMinPINLength); + } + if info.options.ep.is_some() { + configure_ops.push(ConfigureOperation::EnableEnterpriseAttestation); + } + configure_ops + } +} + +impl Display for ConfigureOperation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ConfigureOperation::ToggleAlwaysUV => write!(f, "Toggle option 'Always UV'"), + ConfigureOperation::EnableEnterpriseAttestation => { + write!(f, "Enable Enterprise attestation") + } + ConfigureOperation::SetMinPINLength => write!(f, "Set min. PIN length"), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +enum CredentialsOperation { + List, + Delete, + UpdateUser, +} + +impl CredentialsOperation { + fn ask_user(info: &AuthenticatorInfo, creds: &CredentialList) -> Self { + let sub_level = Self::parse_possible_operations(info, creds); + println!(); + println!("What do you wish to do?"); + let choice = ask_user_choice(&sub_level); + sub_level[choice].clone() + } + + fn parse_possible_operations(info: &AuthenticatorInfo, creds: &CredentialList) -> Vec<Self> { + let mut credentials_ops = vec![]; + if info.options.cred_mgmt == Some(true) + || info.options.credential_mgmt_preview == Some(true) + { + credentials_ops.push(CredentialsOperation::List); + } + if creds.existing_resident_credentials_count > 0 { + credentials_ops.push(CredentialsOperation::Delete); + // FIDO_2_1_PRE devices do not (all?) support UpdateUser. + // So we require devices to support full 2.1 for this. + if info + .versions + .contains(&authenticator::ctap2::commands::get_info::AuthenticatorVersion::FIDO_2_1) + { + credentials_ops.push(CredentialsOperation::UpdateUser); + } + } + credentials_ops + } +} + +impl Display for CredentialsOperation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CredentialsOperation::List => write!(f, "List credentials"), + CredentialsOperation::Delete => write!(f, "Delete credentials"), + CredentialsOperation::UpdateUser => write!(f, "Update user info"), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +enum BioOperation { + ShowInfo, + Add, + List, + Delete, + Rename, +} + +impl BioOperation { + fn ask_user(info: &AuthenticatorInfo) -> Self { + let sub_level = Self::parse_possible_operations(info); + println!(); + println!("What do you wish to do?"); + let choice = ask_user_choice(&sub_level); + sub_level[choice].clone() + } + + fn parse_possible_operations(info: &AuthenticatorInfo) -> Vec<Self> { + let mut bio_ops = vec![]; + if info.options.bio_enroll.is_some() + || info.options.user_verification_mgmt_preview.is_some() + { + bio_ops.extend([BioOperation::ShowInfo, BioOperation::Add]); + } + if info.options.bio_enroll == Some(true) + || info.options.user_verification_mgmt_preview == Some(true) + { + bio_ops.extend([ + BioOperation::Delete, + BioOperation::List, + BioOperation::Rename, + ]); + } + bio_ops + } +} + +impl Display for BioOperation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BioOperation::ShowInfo => write!(f, "Show fingerprint sensor info"), + BioOperation::List => write!(f, "List enrollments"), + BioOperation::Add => write!(f, "Add enrollment"), + BioOperation::Delete => write!(f, "Delete enrollment"), + BioOperation::Rename => write!(f, "Rename enrollment"), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +enum TopLevelOperation { + Quit, + Reset, + ShowFullInfo, + Pin(PinOperation), + Configure, + Credentials, + Bio, +} + +impl Display for TopLevelOperation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TopLevelOperation::Quit => write!(f, "Quit"), + TopLevelOperation::Reset => write!(f, "Reset"), + TopLevelOperation::ShowFullInfo => write!(f, "Show full info"), + TopLevelOperation::Pin(PinOperation::Change) => write!(f, "Change Pin"), + TopLevelOperation::Pin(PinOperation::Set) => write!(f, "Set Pin"), + TopLevelOperation::Configure => write!(f, "Configure Authenticator"), + TopLevelOperation::Credentials => write!(f, "Manage Credentials"), + TopLevelOperation::Bio => write!(f, "Manage BioEnrollments"), + } + } +} + +impl TopLevelOperation { + fn ask_user(info: &AuthenticatorInfo) -> TopLevelOperation { + let top_level = Self::parse_possible_operations(info); + println!(); + println!("What do you wish to do?"); + let choice = ask_user_choice(&top_level); + top_level[choice].clone() + } + + fn parse_possible_operations(info: &AuthenticatorInfo) -> Vec<TopLevelOperation> { + let mut ops = vec![ + TopLevelOperation::Quit, + TopLevelOperation::Reset, + TopLevelOperation::ShowFullInfo, + ]; + + // PIN-related + match info.options.client_pin { + None => {} + Some(true) => ops.push(TopLevelOperation::Pin(PinOperation::Change)), + Some(false) => ops.push(TopLevelOperation::Pin(PinOperation::Set)), + } + + // Authenticator-Configuration + if info.options.authnr_cfg == Some(true) + && (info.options.always_uv.is_some() + || info.options.set_min_pin_length.is_some() + || info.options.ep.is_some()) + { + ops.push(TopLevelOperation::Configure); + } + + // Credential Management + if info.options.cred_mgmt == Some(true) + || info.options.credential_mgmt_preview == Some(true) + { + ops.push(TopLevelOperation::Credentials); + } + + // Bio Enrollment + if info.options.bio_enroll.is_some() + || info.options.user_verification_mgmt_preview.is_some() + { + ops.push(TopLevelOperation::Bio); + } + + ops + } +} + +fn print_usage(program: &str, opts: Options) { + let brief = format!("Usage: {program} [options]"); + print!("{}", opts.usage(&brief)); +} + +fn ask_user_choice<T: Display>(choices: &[T]) -> usize { + for (idx, op) in choices.iter().enumerate() { + println!("({idx}) {op}"); + } + + let mut input = String::new(); + loop { + input.clear(); + print!("Your choice: "); + io::stdout() + .lock() + .flush() + .expect("Failed to flush stdout!"); + io::stdin() + .read_line(&mut input) + .expect("error: unable to read user input"); + if let Ok(idx) = input.trim().parse::<usize>() { + if idx < choices.len() { + // Add a newline in case of success for better separation of in/output + println!(); + return idx; + } + } + } +} + +fn handle_authenticator_config( + res: Option<AuthConfigResult>, + puat: Option<PinUvAuthResult>, + auth_info: &mut Option<AuthenticatorInfo>, + tx: &Sender<InteractiveRequest>, +) { + if let Some(AuthConfigResult::Success(info)) = res { + println!("{:#?}", info.options); + *auth_info = Some(info); + } + let choice = ConfigureOperation::ask_user(auth_info.as_ref().unwrap()); + match choice { + ConfigureOperation::ToggleAlwaysUV => { + tx.send(InteractiveRequest::ChangeConfig( + AuthConfigCommand::ToggleAlwaysUv, + puat, + )) + .expect("Failed to send ToggleAlwaysUV request."); + } + ConfigureOperation::EnableEnterpriseAttestation => { + tx.send(InteractiveRequest::ChangeConfig( + AuthConfigCommand::EnableEnterpriseAttestation, + puat, + )) + .expect("Failed to send EnableEnterpriseAttestation request."); + } + ConfigureOperation::SetMinPINLength => { + let mut length = String::new(); + while length.trim().parse::<u64>().is_err() { + length.clear(); + print!("New minimum PIN length: "); + io::stdout() + .lock() + .flush() + .expect("Failed to flush stdout!"); + io::stdin() + .read_line(&mut length) + .expect("error: unable to read user input"); + } + let new_length = length.trim().parse::<u64>().unwrap(); + let cmd = SetMinPINLength { + new_min_pin_length: Some(new_length), + min_pin_length_rpids: None, + force_change_pin: None, + }; + + tx.send(InteractiveRequest::ChangeConfig( + AuthConfigCommand::SetMinPINLength(cmd), + puat, + )) + .expect("Failed to send SetMinPINLength request."); + } + } +} + +fn handle_credential_management( + res: Option<CredentialManagementResult>, + puat: Option<PinUvAuthResult>, + auth_info: &mut Option<AuthenticatorInfo>, + tx: &Sender<InteractiveRequest>, +) { + match res { + Some(CredentialManagementResult::CredentialList(credlist)) => { + let mut creds = vec![]; + for rp in &credlist.credential_list { + for cred in &rp.credentials { + creds.push(( + rp.rp.name.clone(), + cred.user.clone(), + cred.credential_id.clone(), + )); + } + } + let display_creds: Vec<_> = creds + .iter() + .map(|(rp, user, id)| format!("{:?} - {:?} - {:?}", rp, user, id)) + .collect(); + for (idx, op) in display_creds.iter().enumerate() { + println!("({idx}) {op}"); + } + + loop { + match CredentialsOperation::ask_user(auth_info.as_ref().unwrap(), &credlist) { + CredentialsOperation::List => { + let mut idx = 0; + for rp in credlist.credential_list.iter() { + for cred in &rp.credentials { + println!("({idx}) - {:?}: {:?}", rp.rp.name, cred); + idx += 1; + } + } + continue; + } + CredentialsOperation::Delete => { + let choice = ask_user_choice(&display_creds); + tx.send(InteractiveRequest::CredentialManagement( + CredManagementCmd::DeleteCredential(creds[choice].2.clone()), + puat, + )) + .expect("Failed to send DeleteCredentials request."); + break; + } + CredentialsOperation::UpdateUser => { + let choice = ask_user_choice(&display_creds); + // Updating username. Asking for the new one. + let mut input = String::new(); + print!("New username: "); + io::stdout() + .lock() + .flush() + .expect("Failed to flush stdout!"); + io::stdin() + .read_line(&mut input) + .expect("error: unable to read user input"); + input = input.trim().to_string(); + let name = if input.is_empty() { None } else { Some(input) }; + let mut new_user = creds[choice].1.clone(); + new_user.name = name; + tx.send(InteractiveRequest::CredentialManagement( + CredManagementCmd::UpdateUserInformation( + creds[choice].2.clone(), + new_user, + ), + puat, + )) + .expect("Failed to send UpdateUserinformation request."); + break; + } + } + } + } + None + | Some(CredentialManagementResult::DeleteSucess) + | Some(CredentialManagementResult::UpdateSuccess) => { + tx.send(InteractiveRequest::CredentialManagement( + CredManagementCmd::GetCredentials, + puat, + )) + .expect("Failed to send GetCredentials request."); + } + } +} + +fn ask_user_bio_options( + biolist: Vec<EnrollmentInfo>, + puat: Option<PinUvAuthResult>, + auth_info: &mut Option<AuthenticatorInfo>, + tx: &Sender<InteractiveRequest>, +) { + let display_bios: Vec<_> = biolist + .iter() + .map(|x| format!("{:?}: {:?}", x.template_friendly_name, x.template_id)) + .collect(); + loop { + match BioOperation::ask_user(auth_info.as_ref().unwrap()) { + BioOperation::List => { + for (idx, bio) in display_bios.iter().enumerate() { + println!("({idx}) - {bio}"); + } + continue; + } + BioOperation::ShowInfo => { + tx.send(InteractiveRequest::BioEnrollment( + BioEnrollmentCmd::GetFingerprintSensorInfo, + puat, + )) + .expect("Failed to send GetFingerprintSensorInfo request."); + break; + } + BioOperation::Delete => { + let choice = ask_user_choice(&display_bios); + tx.send(InteractiveRequest::BioEnrollment( + BioEnrollmentCmd::DeleteEnrollment(biolist[choice].template_id.clone()), + puat, + )) + .expect("Failed to send GetEnrollments request."); + break; + } + BioOperation::Rename => { + let choice = ask_user_choice(&display_bios); + let chosen_id = biolist[choice].template_id.clone(); + // Updating enrollment name. Asking for the new one. + let mut input = String::new(); + print!("New name: "); + io::stdout() + .lock() + .flush() + .expect("Failed to flush stdout!"); + io::stdin() + .read_line(&mut input) + .expect("error: unable to read user input"); + let name = input.trim().to_string(); + tx.send(InteractiveRequest::BioEnrollment( + BioEnrollmentCmd::ChangeName(chosen_id, name), + puat, + )) + .expect("Failed to send GetEnrollments request."); + break; + } + BioOperation::Add => { + let mut input = String::new(); + print!( + "The name of the new bio enrollment (leave empty if you don't want to name it): " + ); + io::stdout() + .lock() + .flush() + .expect("Failed to flush stdout!"); + io::stdin() + .read_line(&mut input) + .expect("error: unable to read user input"); + input = input.trim().to_string(); + let name = if input.is_empty() { None } else { Some(input) }; + tx.send(InteractiveRequest::BioEnrollment( + BioEnrollmentCmd::StartNewEnrollment(name), + puat, + )) + .expect("Failed to send StartNewEnrollment request."); + break; + } + } + } +} + +fn handle_bio_enrollments( + res: Option<BioEnrollmentResult>, + puat: Option<PinUvAuthResult>, + auth_info: &mut Option<AuthenticatorInfo>, + tx: &Sender<InteractiveRequest>, +) { + match res { + Some(BioEnrollmentResult::EnrollmentList(biolist)) => { + ask_user_bio_options(biolist, puat, auth_info, tx); + } + None => { + if BioOperation::parse_possible_operations(auth_info.as_ref().unwrap()) + .contains(&BioOperation::List) + { + tx.send(InteractiveRequest::BioEnrollment( + BioEnrollmentCmd::GetEnrollments, + puat, + )) + .expect("Failed to send GetEnrollments request."); + } else { + ask_user_bio_options(vec![], puat, auth_info, tx); + } + } + Some(BioEnrollmentResult::UpdateSuccess) => { + tx.send(InteractiveRequest::BioEnrollment( + BioEnrollmentCmd::GetEnrollments, + puat, + )) + .expect("Failed to send GetEnrollments request."); + } + Some(BioEnrollmentResult::DeleteSuccess(info)) => { + *auth_info = Some(info.clone()); + if BioOperation::parse_possible_operations(&info).contains(&BioOperation::List) { + tx.send(InteractiveRequest::BioEnrollment( + BioEnrollmentCmd::GetEnrollments, + puat, + )) + .expect("Failed to send GetEnrollments request."); + } else { + ask_user_bio_options(vec![], puat, auth_info, tx); + } + } + Some(BioEnrollmentResult::AddSuccess(info)) => { + *auth_info = Some(info); + tx.send(InteractiveRequest::BioEnrollment( + BioEnrollmentCmd::GetEnrollments, + puat, + )) + .expect("Failed to send GetEnrollments request."); + } + Some(BioEnrollmentResult::FingerprintSensorInfo(info)) => { + println!("{info:#?}"); + if BioOperation::parse_possible_operations(auth_info.as_ref().unwrap()) + .contains(&BioOperation::List) + { + tx.send(InteractiveRequest::BioEnrollment( + BioEnrollmentCmd::GetEnrollments, + puat, + )) + .expect("Failed to send GetEnrollments request."); + } else { + ask_user_bio_options(vec![], puat, auth_info, tx); + } + } + Some(BioEnrollmentResult::SampleStatus(last_sample_status, remaining_samples)) => { + println!("Last sample status: {last_sample_status:?}, remaining samples: {remaining_samples:?}"); + } + } +} + +fn interactive_status_callback(status_rx: Receiver<StatusUpdate>) { + let mut tx = None; + let mut auth_info = None; + loop { + match status_rx.recv() { + Ok(StatusUpdate::InteractiveManagement(InteractiveUpdate::StartManagement(( + tx_new, + auth_info_new, + )))) => { + let info = match auth_info_new { + Some(info) => info, + None => { + println!("Device only supports CTAP1 and can't be managed."); + return; + } + }; + auth_info = Some(info.clone()); + tx = Some(tx_new); + match TopLevelOperation::ask_user(&info) { + TopLevelOperation::Quit => { + tx.as_ref() + .unwrap() + .send(InteractiveRequest::Quit) + .expect("Failed to send PIN-set request"); + } + TopLevelOperation::Reset => tx + .as_ref() + .unwrap() + .send(InteractiveRequest::Reset) + .expect("Failed to send Reset request."), + TopLevelOperation::ShowFullInfo => { + println!("Authenticator Info {:#?}", info); + return; + } + TopLevelOperation::Pin(op) => { + let raw_new_pin = rpassword::prompt_password_stderr("Enter new PIN: ") + .expect("Failed to read PIN"); + let new_pin = Pin::new(&raw_new_pin); + if op == PinOperation::Change { + let raw_curr_pin = + rpassword::prompt_password_stderr("Enter current PIN: ") + .expect("Failed to read PIN"); + let curr_pin = Pin::new(&raw_curr_pin); + tx.as_ref() + .unwrap() + .send(InteractiveRequest::ChangePIN(curr_pin, new_pin)) + .expect("Failed to send PIN-change request"); + } else { + tx.as_ref() + .unwrap() + .send(InteractiveRequest::SetPIN(new_pin)) + .expect("Failed to send PIN-set request"); + } + } + TopLevelOperation::Configure => handle_authenticator_config( + None, + None, + &mut auth_info, + tx.as_ref().unwrap(), + ), + TopLevelOperation::Credentials => handle_credential_management( + None, + None, + &mut auth_info, + tx.as_ref().unwrap(), + ), + TopLevelOperation::Bio => { + handle_bio_enrollments(None, None, &mut auth_info, tx.as_ref().unwrap()) + } + } + continue; + } + Ok(StatusUpdate::InteractiveManagement(InteractiveUpdate::AuthConfigUpdate(( + cfg_result, + puat_res, + )))) => { + handle_authenticator_config( + Some(cfg_result), + puat_res, + &mut auth_info, + tx.as_ref().unwrap(), + ); + continue; + } + Ok(StatusUpdate::InteractiveManagement( + InteractiveUpdate::CredentialManagementUpdate((cfg_result, puat_res)), + )) => { + handle_credential_management( + Some(cfg_result), + puat_res, + &mut auth_info, + tx.as_ref().unwrap(), + ); + continue; + } + Ok(StatusUpdate::InteractiveManagement(InteractiveUpdate::BioEnrollmentUpdate(( + bio_res, + puat_res, + )))) => { + handle_bio_enrollments( + Some(bio_res), + puat_res, + &mut auth_info, + tx.as_ref().unwrap(), + ); + continue; + } + Ok(StatusUpdate::SelectDeviceNotice) => { + println!("STATUS: Please select a device by touching one of them."); + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => { + let raw_pin = + rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN"); + sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN"); + continue; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => { + println!( + "Wrong PIN! {}", + attempts.map_or("Try again.".to_string(), |a| format!( + "You have {a} attempts left." + )) + ); + let raw_pin = + rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN"); + sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN"); + continue; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => { + panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.") + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => { + panic!("Too many failed attempts. Your device has been blocked. Reset it.") + } + Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => { + println!( + "Wrong UV! {}", + attempts.map_or("Try again.".to_string(), |a| format!( + "You have {a} attempts left." + )) + ); + continue; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => { + println!("Too many failed UV-attempts."); + continue; + } + Ok(StatusUpdate::PresenceRequired) => { + println!("Please touch your device!"); + continue; + } + Ok(StatusUpdate::PinUvError(e)) => { + panic!("Unexpected error: {:?}", e) + } + Ok(StatusUpdate::SelectResultNotice(_, _)) => { + panic!("Unexpected select device notice") + } + Err(RecvError) => { + println!("STATUS: end"); + return; + } + } + } +} + +fn main() { + env_logger::init(); + + let args: Vec<String> = env::args().collect(); + let program = args[0].clone(); + + let mut opts = Options::new(); + opts.optflag("h", "help", "print this help menu").optopt( + "t", + "timeout", + "timeout in seconds", + "SEC", + ); + opts.optflag("h", "help", "print this help menu"); + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(f) => panic!("{}", f.to_string()), + }; + if matches.opt_present("help") { + print_usage(&program, opts); + return; + } + + let mut manager = + AuthenticatorService::new().expect("The auth service should initialize safely"); + manager.add_u2f_usb_hid_platform_transports(); + + let timeout_ms = match matches.opt_get_default::<u64>("timeout", 120) { + Ok(timeout_s) => { + println!("Using {}s as the timeout", &timeout_s); + timeout_s * 1_000 + } + Err(e) => { + println!("{e}"); + print_usage(&program, opts); + return; + } + }; + + let (status_tx, status_rx) = channel::<StatusUpdate>(); + thread::spawn(move || interactive_status_callback(status_rx)); + + let (manage_tx, manage_rx) = channel(); + let state_callback = + StateCallback::<Result<ManageResult, AuthenticatorError>>::new(Box::new(move |rv| { + manage_tx.send(rv).unwrap(); + })); + + match manager.manage(timeout_ms, status_tx, state_callback) { + Ok(_) => { + debug!("Started management"); + } + Err(e) => { + println!("Error! Failed to start interactive management: {:?}", e); + return; + } + } + let manage_result = manage_rx + .recv() + .expect("Problem receiving, unable to continue"); + match manage_result { + Ok(r) => { + println!("Success! Result = {r:?}"); + } + Err(e) => { + println!("Error! {:?}", e); + } + }; + println!("Done"); +} diff --git a/third_party/rust/authenticator/examples/reset.rs b/third_party/rust/authenticator/examples/reset.rs new file mode 100644 index 0000000000..867ab5448f --- /dev/null +++ b/third_party/rust/authenticator/examples/reset.rs @@ -0,0 +1,133 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use authenticator::{ + authenticatorservice::AuthenticatorService, + ctap2::commands::StatusCode, + errors::{AuthenticatorError, CommandError, HIDError}, + statecallback::StateCallback, + StatusUpdate, +}; +use getopts::Options; +use std::env; +use std::sync::mpsc::{channel, RecvError}; + +fn print_usage(program: &str, opts: Options) { + let brief = format!("Usage: {program} [options]"); + print!("{}", opts.usage(&brief)); +} + +fn main() { + env_logger::init(); + + let args: Vec<String> = env::args().collect(); + let program = args[0].clone(); + + let mut opts = Options::new(); + opts.optflag("h", "help", "print this help menu").optopt( + "t", + "timeout", + "timeout in seconds", + "SEC", + ); + opts.optflag("h", "help", "print this help menu"); + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(f) => panic!("{}", f.to_string()), + }; + if matches.opt_present("help") { + print_usage(&program, opts); + return; + } + + let mut manager = + AuthenticatorService::new().expect("The auth service should initialize safely"); + manager.add_u2f_usb_hid_platform_transports(); + + let timeout_ms = match matches.opt_get_default::<u64>("timeout", 25) { + Ok(timeout_s) => { + println!("Using {}s as the timeout", &timeout_s); + timeout_s * 1_000 + } + Err(e) => { + println!("{e}"); + print_usage(&program, opts); + return; + } + }; + + println!( + "NOTE: Please unplug all devices, type in 'yes' and plug in the device that should be reset." + ); + loop { + let mut s = String::new(); + println!("ATTENTION: Resetting a device will wipe all credentials! Do you wish to continue? [yes/N]"); + std::io::stdin() + .read_line(&mut s) + .expect("Did not enter a correct string"); + let trimmed = s.trim(); + if trimmed.is_empty() || trimmed == "N" || trimmed == "n" { + println!("Exiting without reset."); + return; + } + if trimmed == "y" { + println!("Please type in the whole word 'yes'"); + continue; + } + if trimmed == "yes" { + break; + } + } + + let (status_tx, status_rx) = channel::<StatusUpdate>(); + let (reset_tx, reset_rx) = channel(); + let rs_tx = reset_tx; + let callback = StateCallback::new(Box::new(move |rv| { + let _ = rs_tx.send(rv); + })); + + if let Err(e) = manager.reset(timeout_ms, status_tx, callback) { + panic!("Couldn't register: {:?}", e); + }; + + loop { + match status_rx.recv() { + Ok(StatusUpdate::SelectDeviceNotice) => { + println!("ERROR: Please unplug all other tokens that should not be reset!"); + // Needed to give the tokens enough time to start blinking + // otherwise we may cancel pre-maturely and this binary will hang + std::thread::sleep(std::time::Duration::from_millis(200)); + manager.cancel().unwrap(); + return; + } + Ok(StatusUpdate::PresenceRequired) => { + println!("STATUS: waiting for user presence"); + break; + } + Ok(StatusUpdate::PinUvError(..)) => panic!("Reset should never ask for a PIN!"), + Ok(_) => { /* Ignore all other updates */ } + Err(RecvError) => { + println!("RecvError"); + return; + } + } + } + + let reset_result = reset_rx + .recv() + .expect("Problem receiving, unable to continue"); + match reset_result { + Ok(()) => { + println!("Token successfully reset!"); + } + Err(AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode( + StatusCode::NotAllowed, + _, + )))) => { + println!("Resetting is only allowed within the first 10 seconds after powering up."); + println!("Please unplug your device, plug it back in and try again."); + } + Err(e) => panic!("Reset failed: {:?}", e), + }; +} diff --git a/third_party/rust/authenticator/examples/set_pin.rs b/third_party/rust/authenticator/examples/set_pin.rs new file mode 100644 index 0000000000..5534ca08fd --- /dev/null +++ b/third_party/rust/authenticator/examples/set_pin.rs @@ -0,0 +1,146 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use authenticator::{ + authenticatorservice::AuthenticatorService, statecallback::StateCallback, Pin, StatusPinUv, + StatusUpdate, +}; +use getopts::Options; +use std::sync::mpsc::{channel, RecvError}; +use std::{env, thread}; + +fn print_usage(program: &str, opts: Options) { + let brief = format!("Usage: {program} [options]"); + print!("{}", opts.usage(&brief)); +} + +fn main() { + env_logger::init(); + + let args: Vec<String> = env::args().collect(); + let program = args[0].clone(); + + let mut opts = Options::new(); + opts.optflag("h", "help", "print this help menu").optopt( + "t", + "timeout", + "timeout in seconds", + "SEC", + ); + opts.optflag("h", "help", "print this help menu"); + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(f) => panic!("{}", f.to_string()), + }; + if matches.opt_present("help") { + print_usage(&program, opts); + return; + } + + let mut manager = + AuthenticatorService::new().expect("The auth service should initialize safely"); + manager.add_u2f_usb_hid_platform_transports(); + + let timeout_ms = match matches.opt_get_default::<u64>("timeout", 25) { + Ok(timeout_s) => { + println!("Using {}s as the timeout", &timeout_s); + timeout_s * 1_000 + } + Err(e) => { + println!("{e}"); + print_usage(&program, opts); + return; + } + }; + + let new_pin = rpassword::prompt_password_stderr("Enter new PIN: ").expect("Failed to read PIN"); + let repeat_new_pin = + rpassword::prompt_password_stderr("Enter it again: ").expect("Failed to read PIN"); + if new_pin != repeat_new_pin { + println!("PINs did not match!"); + return; + } + + let (status_tx, status_rx) = channel::<StatusUpdate>(); + thread::spawn(move || loop { + match status_rx.recv() { + Ok(StatusUpdate::InteractiveManagement(..)) => { + panic!("STATUS: This can't happen when doing non-interactive usage"); + } + Ok(StatusUpdate::SelectDeviceNotice) => { + println!("STATUS: Please select a device by touching one of them."); + } + Ok(StatusUpdate::PresenceRequired) => { + println!("STATUS: waiting for user presence"); + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => { + let raw_pin = + rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN"); + sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN"); + continue; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => { + println!( + "Wrong PIN! {}", + attempts.map_or("Try again.".to_string(), |a| format!( + "You have {a} attempts left." + )) + ); + let raw_pin = + rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN"); + sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN"); + continue; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => { + panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.") + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => { + panic!("Too many failed attempts. Your device has been blocked. Reset it.") + } + Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => { + println!( + "Wrong UV! {}", + attempts.map_or("Try again.".to_string(), |a| format!( + "You have {a} attempts left." + )) + ); + continue; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => { + println!("Too many failed UV-attempts."); + continue; + } + Ok(StatusUpdate::PinUvError(e)) => { + panic!("Unexpected error: {:?}", e) + } + Ok(StatusUpdate::SelectResultNotice(_, _)) => { + panic!("Unexpected select device notice") + } + Err(RecvError) => { + println!("STATUS: end"); + return; + } + } + }); + + let (reset_tx, reset_rx) = channel(); + let rs_tx = reset_tx; + let callback = StateCallback::new(Box::new(move |rv| { + let _ = rs_tx.send(rv); + })); + + if let Err(e) = manager.set_pin(timeout_ms, Pin::new(&new_pin), status_tx, callback) { + panic!("Couldn't call set_pin: {:?}", e); + }; + + let reset_result = reset_rx + .recv() + .expect("Problem receiving, unable to continue"); + match reset_result { + Ok(()) => { + println!("PIN successfully set!"); + } + Err(e) => panic!("Setting PIN failed: {:?}", e), + }; +} diff --git a/third_party/rust/authenticator/examples/test_exclude_list.rs b/third_party/rust/authenticator/examples/test_exclude_list.rs new file mode 100644 index 0000000000..e24c49d05a --- /dev/null +++ b/third_party/rust/authenticator/examples/test_exclude_list.rs @@ -0,0 +1,309 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use authenticator::{ + authenticatorservice::{AuthenticatorService, RegisterArgs, SignArgs}, + crypto::COSEAlgorithm, + ctap2::commands::StatusCode, + ctap2::server::{ + PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, + PublicKeyCredentialUserEntity, RelyingParty, ResidentKeyRequirement, Transport, + UserVerificationRequirement, + }, + errors::{AuthenticatorError, CommandError, HIDError, UnsupportedOption}, + statecallback::StateCallback, + Pin, StatusPinUv, StatusUpdate, +}; + +use getopts::Options; +use sha2::{Digest, Sha256}; +use std::sync::mpsc::{channel, RecvError}; +use std::{env, thread}; + +fn print_usage(program: &str, opts: Options) { + let brief = format!("Usage: {program} [options]"); + print!("{}", opts.usage(&brief)); +} + +fn main() { + env_logger::init(); + + let args: Vec<String> = env::args().collect(); + let program = args[0].clone(); + + let mut opts = Options::new(); + opts.optflag("h", "help", "print this help menu").optopt( + "t", + "timeout", + "timeout in seconds", + "SEC", + ); + opts.optflag("h", "help", "print this help menu"); + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(f) => panic!("{}", f.to_string()), + }; + if matches.opt_present("help") { + print_usage(&program, opts); + return; + } + + let mut manager = + AuthenticatorService::new().expect("The auth service should initialize safely"); + + manager.add_u2f_usb_hid_platform_transports(); + + let timeout_ms = match matches.opt_get_default::<u64>("timeout", 25) { + Ok(timeout_s) => { + println!("Using {}s as the timeout", &timeout_s); + timeout_s * 1_000 + } + Err(e) => { + println!("{e}"); + print_usage(&program, opts); + return; + } + }; + + println!("Asking a security key to register now..."); + let challenge_str = format!( + "{}{}", + r#"{"challenge": "1vQ9mxionq0ngCnjD-wTsv1zUSrGRtFqG2xP09SbZ70","#, + r#" "version": "U2F_V2", "appId": "http://example.com"}"# + ); + let mut challenge = Sha256::new(); + challenge.update(challenge_str.as_bytes()); + let chall_bytes = challenge.finalize().into(); + + let (status_tx, status_rx) = channel::<StatusUpdate>(); + thread::spawn(move || loop { + match status_rx.recv() { + Ok(StatusUpdate::InteractiveManagement(..)) => { + panic!("STATUS: This can't happen when doing non-interactive usage"); + } + Ok(StatusUpdate::SelectDeviceNotice) => { + println!("STATUS: Please select a device by touching one of them."); + } + Ok(StatusUpdate::PresenceRequired) => { + println!("STATUS: waiting for user presence"); + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => { + let raw_pin = + rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN"); + sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN"); + continue; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => { + println!( + "Wrong PIN! {}", + attempts.map_or("Try again.".to_string(), |a| format!( + "You have {a} attempts left." + )) + ); + let raw_pin = + rpassword::prompt_password_stderr("Enter PIN: ").expect("Failed to read PIN"); + sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN"); + continue; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => { + panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.") + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => { + panic!("Too many failed attempts. Your device has been blocked. Reset it.") + } + Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(attempts))) => { + println!( + "Wrong UV! {}", + attempts.map_or("Try again.".to_string(), |a| format!( + "You have {a} attempts left." + )) + ); + continue; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => { + println!("Too many failed UV-attempts."); + continue; + } + Ok(StatusUpdate::PinUvError(e)) => { + panic!("Unexpected error: {:?}", e) + } + Ok(StatusUpdate::SelectResultNotice(_, _)) => { + panic!("Unexpected select device notice") + } + Err(RecvError) => { + println!("STATUS: end"); + return; + } + } + }); + + let user = PublicKeyCredentialUserEntity { + id: "user_id".as_bytes().to_vec(), + name: Some("A. User".to_string()), + display_name: None, + }; + let origin = "https://example.com".to_string(); + let mut ctap_args = RegisterArgs { + client_data_hash: chall_bytes, + relying_party: RelyingParty { + id: "example.com".to_string(), + name: None, + }, + origin: origin.clone(), + user, + pub_cred_params: vec![ + PublicKeyCredentialParameters { + alg: COSEAlgorithm::ES256, + }, + PublicKeyCredentialParameters { + alg: COSEAlgorithm::RS256, + }, + ], + exclude_list: vec![], + user_verification_req: UserVerificationRequirement::Preferred, + resident_key_req: ResidentKeyRequirement::Discouraged, + extensions: Default::default(), + pin: None, + use_ctap1_fallback: false, + }; + + let mut registered_key_handle = None; + loop { + let (register_tx, register_rx) = channel(); + let callback = StateCallback::new(Box::new(move |rv| { + register_tx.send(rv).unwrap(); + })); + + if let Err(e) = manager.register(timeout_ms, ctap_args.clone(), status_tx.clone(), callback) + { + panic!("Couldn't register: {:?}", e); + }; + + let register_result = register_rx + .recv() + .expect("Problem receiving, unable to continue"); + match register_result { + Ok(a) => { + println!("Ok!"); + println!("Registering again with the key_handle we just got back. This should result in a 'already registered' error."); + let key_handle = a + .att_obj + .auth_data + .credential_data + .unwrap() + .credential_id + .clone(); + let pub_key = PublicKeyCredentialDescriptor { + id: key_handle, + transports: vec![Transport::USB], + }; + ctap_args.exclude_list = vec![pub_key.clone()]; + registered_key_handle = Some(pub_key); + continue; + } + Err(AuthenticatorError::CredentialExcluded) => { + println!("Got an 'already registered' error, as expected."); + if ctap_args.exclude_list.len() > 1 { + println!("Quitting."); + break; + } + println!("Extending the list to contain more invalid handles."); + let registered_handle = ctap_args.exclude_list[0].clone(); + ctap_args.exclude_list = vec![]; + for ii in 0..10 { + ctap_args.exclude_list.push(PublicKeyCredentialDescriptor { + id: vec![ii; 50], + transports: vec![Transport::USB], + }); + } + ctap_args.exclude_list.push(registered_handle); + continue; + } + Err(e) => panic!("Registration failed: {:?}", e), + }; + } + + // Signing + let mut ctap_args = SignArgs { + client_data_hash: chall_bytes, + origin, + relying_party_id: "example.com".to_string(), + allow_list: vec![], + extensions: Default::default(), + pin: None, + use_ctap1_fallback: false, + user_verification_req: UserVerificationRequirement::Preferred, + user_presence_req: true, + }; + + let mut no_cred_errors_done = false; + loop { + let (sign_tx, sign_rx) = channel(); + let callback = StateCallback::new(Box::new(move |rv| { + sign_tx.send(rv).unwrap(); + })); + + if let Err(e) = manager.sign(timeout_ms, ctap_args.clone(), status_tx.clone(), callback) { + panic!("Couldn't sign: {:?}", e); + }; + + let sign_result = sign_rx + .recv() + .expect("Problem receiving, unable to continue"); + match sign_result { + Ok(_) => { + if !no_cred_errors_done { + panic!("Should have errored out with NoCredentials, but it succeeded."); + } + println!("Successfully signed!"); + if ctap_args.allow_list.len() > 1 { + println!("Quitting."); + break; + } + println!("Signing again with a long allow_list that needs pre-flighting."); + let registered_handle = registered_key_handle.as_ref().unwrap().clone(); + ctap_args.allow_list = vec![]; + for ii in 0..10 { + ctap_args.allow_list.push(PublicKeyCredentialDescriptor { + id: vec![ii; 50], + transports: vec![Transport::USB], + }); + } + ctap_args.allow_list.push(registered_handle); + continue; + } + Err(AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode( + StatusCode::NoCredentials, + None, + )))) + | Err(AuthenticatorError::UnsupportedOption(UnsupportedOption::EmptyAllowList)) => { + if ctap_args.allow_list.is_empty() { + // Try again with a list of false creds. We should end up here again. + println!( + "Got an 'no credentials' error, as expected with an empty allow-list." + ); + println!("Extending the list to contain only fake handles."); + ctap_args.allow_list = vec![]; + for ii in 0..10 { + ctap_args.allow_list.push(PublicKeyCredentialDescriptor { + id: vec![ii; 50], + transports: vec![Transport::USB], + }); + } + } else { + println!( + "Got an 'no credentials' error, as expected with an all-fake allow-list." + ); + println!("Extending the list to contain one valid handle."); + let registered_handle = registered_key_handle.as_ref().unwrap().clone(); + ctap_args.allow_list = vec![registered_handle]; + no_cred_errors_done = true; + } + continue; + } + Err(e) => panic!("Registration failed: {:?}", e), + }; + } + println!("Done") +} |