mirror of
https://github.com/librekeys/picoforge.git
synced 2026-07-28 08:01:19 -07:00
Add fingerprint management to PicoForge
This commit is contained in:
@@ -12,6 +12,7 @@ pub enum CtapCommand {
|
||||
ClientPin = 0x06,
|
||||
Reset = 0x07,
|
||||
GetNextAssertion = 0x08,
|
||||
BioEnroll = 0x09,
|
||||
CredentialMgmt = 0x0A,
|
||||
Selection = 0x0B,
|
||||
LargeBlobs = 0x0C,
|
||||
@@ -176,6 +177,50 @@ pub enum CredentialMgmtResponseParam {
|
||||
TotalCredentials = 0x09,
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BioEnrollmentParam {
|
||||
Modality = 0x01,
|
||||
SubCommand = 0x02,
|
||||
SubCommandParams = 0x03,
|
||||
PinUvAuthProtocol = 0x04,
|
||||
PinUvAuthParam = 0x05,
|
||||
GetModality = 0x06,
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BioEnrollmentSubCommand {
|
||||
EnrollBegin = 0x01,
|
||||
EnrollCaptureNextSample = 0x02,
|
||||
CancelCurrentEnrollment = 0x03,
|
||||
EnumerateEnrollments = 0x04,
|
||||
SetFriendlyName = 0x05,
|
||||
RemoveEnrollment = 0x06,
|
||||
GetFingerprintSensorInfo = 0x07,
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BioEnrollmentSubParam {
|
||||
TemplateId = 0x01,
|
||||
FriendlyName = 0x02,
|
||||
TimeoutMilliseconds = 0x03,
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BioEnrollmentResponseParam {
|
||||
Modality = 0x01,
|
||||
FingerprintKind = 0x02,
|
||||
MaxCaptureSamplesRequiredForEnroll = 0x03,
|
||||
TemplateId = 0x04,
|
||||
LastEnrollSampleStatus = 0x05,
|
||||
RemainingSamples = 0x06,
|
||||
TemplateInfos = 0x07,
|
||||
MaxTemplateFriendlyName = 0x08,
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConfigSubCommandParam {
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::collections::BTreeMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::device::fido::constants::*;
|
||||
use crate::device::types::{FingerprintSensorInfo, FingerprintTemplate};
|
||||
use crate::error::PFError;
|
||||
|
||||
// HID Transport Constants
|
||||
@@ -51,6 +52,13 @@ pub struct EnumerateCredentialResponse {
|
||||
pub total_credentials: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct BioEnrollmentResponse {
|
||||
pub template_id: Option<Vec<u8>>,
|
||||
pub status: Option<u8>,
|
||||
pub remaining_samples: Option<u32>,
|
||||
}
|
||||
|
||||
impl HidTransport {
|
||||
pub fn open() -> Result<Self, PFError> {
|
||||
log::info!("Attempting to open HID transport for FIDO device...");
|
||||
@@ -1493,6 +1501,337 @@ impl HidTransport {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn bio_enrollment_get_fingerprint_sensor_info(
|
||||
&self,
|
||||
) -> Result<FingerprintSensorInfo, PFError> {
|
||||
let response = self.send_bio_enrollment(
|
||||
None,
|
||||
BioEnrollmentSubCommand::GetFingerprintSensorInfo,
|
||||
None,
|
||||
)?;
|
||||
|
||||
let map = match response {
|
||||
Value::Map(m) => m,
|
||||
_ => {
|
||||
return Err(PFError::Device(
|
||||
"Unexpected bio sensor info response format".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let fingerprint_kind = match map.get(&Value::Integer(
|
||||
BioEnrollmentResponseParam::FingerprintKind as i128,
|
||||
)) {
|
||||
Some(Value::Integer(1)) => "touch".to_string(),
|
||||
Some(Value::Integer(2)) => "swipe".to_string(),
|
||||
Some(Value::Integer(v)) => format!("unknown ({})", v),
|
||||
_ => "unknown".to_string(),
|
||||
};
|
||||
|
||||
let max_capture_samples_required_for_enroll = match map.get(&Value::Integer(
|
||||
BioEnrollmentResponseParam::MaxCaptureSamplesRequiredForEnroll as i128,
|
||||
)) {
|
||||
Some(Value::Integer(v)) => *v as u32,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
let max_template_friendly_name = match map.get(&Value::Integer(
|
||||
BioEnrollmentResponseParam::MaxTemplateFriendlyName as i128,
|
||||
)) {
|
||||
Some(Value::Integer(v)) => *v as u32,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
Ok(FingerprintSensorInfo {
|
||||
modality: "fingerprint".to_string(),
|
||||
fingerprint_kind,
|
||||
max_capture_samples_required_for_enroll,
|
||||
max_template_friendly_name,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn bio_enrollment_enumerate_enrollments(
|
||||
&self,
|
||||
pin: &str,
|
||||
) -> Result<Vec<FingerprintTemplate>, PFError> {
|
||||
let pin_token = self.get_pin_token_with_permission(
|
||||
pin,
|
||||
PinUvAuthTokenPermissions::BIO_ENROLLMENT,
|
||||
None,
|
||||
)?;
|
||||
|
||||
let response = self.send_bio_enrollment(
|
||||
Some(&pin_token),
|
||||
BioEnrollmentSubCommand::EnumerateEnrollments,
|
||||
None,
|
||||
)?;
|
||||
|
||||
let map = match response {
|
||||
Value::Map(m) => m,
|
||||
_ => {
|
||||
return Err(PFError::Device(
|
||||
"Unexpected fingerprint enumeration response format".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let templates = match map.get(&Value::Integer(
|
||||
BioEnrollmentResponseParam::TemplateInfos as i128,
|
||||
)) {
|
||||
Some(Value::Array(items)) => items
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
Value::Map(template_map) => {
|
||||
let template_id = match template_map
|
||||
.get(&Value::Integer(BioEnrollmentSubParam::TemplateId as i128))
|
||||
{
|
||||
Some(Value::Bytes(bytes)) => hex::encode_upper(bytes),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let friendly_name = match template_map.get(&Value::Integer(
|
||||
BioEnrollmentSubParam::FriendlyName as i128,
|
||||
)) {
|
||||
Some(Value::Text(name)) if !name.is_empty() => Some(name.clone()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Some(FingerprintTemplate {
|
||||
template_id,
|
||||
friendly_name,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
Ok(templates)
|
||||
}
|
||||
|
||||
pub fn bio_enrollment_begin(
|
||||
&self,
|
||||
pin: &str,
|
||||
timeout_ms: Option<u16>,
|
||||
) -> Result<(Vec<u8>, BioEnrollmentResponse), PFError> {
|
||||
let pin_token = self.get_pin_token_with_permission(
|
||||
pin,
|
||||
PinUvAuthTokenPermissions::BIO_ENROLLMENT,
|
||||
None,
|
||||
)?;
|
||||
|
||||
let begin_sub_params = timeout_ms.map(|timeout| {
|
||||
let mut sub_params = BTreeMap::new();
|
||||
sub_params.insert(
|
||||
Value::Integer(BioEnrollmentSubParam::TimeoutMilliseconds as i128),
|
||||
Value::Integer(timeout as i128),
|
||||
);
|
||||
Value::Map(sub_params)
|
||||
});
|
||||
|
||||
let response = self.send_bio_enrollment(
|
||||
Some(&pin_token),
|
||||
BioEnrollmentSubCommand::EnrollBegin,
|
||||
begin_sub_params,
|
||||
)?;
|
||||
|
||||
let parsed = self.parse_bio_enrollment_response(&response)?;
|
||||
let template_id = parsed
|
||||
.template_id
|
||||
.clone()
|
||||
.ok_or_else(|| PFError::Device("Enrollment did not return a template id".into()))?;
|
||||
|
||||
Ok((template_id, parsed))
|
||||
}
|
||||
|
||||
pub fn bio_enrollment_next(
|
||||
&self,
|
||||
pin: &str,
|
||||
timeout_ms: Option<u16>,
|
||||
) -> Result<BioEnrollmentResponse, PFError> {
|
||||
let pin_token = self.get_pin_token_with_permission(
|
||||
pin,
|
||||
PinUvAuthTokenPermissions::BIO_ENROLLMENT,
|
||||
None,
|
||||
)?;
|
||||
|
||||
let next_sub_params = timeout_ms.map(|timeout| {
|
||||
let mut sub_params = BTreeMap::new();
|
||||
sub_params.insert(
|
||||
Value::Integer(BioEnrollmentSubParam::TimeoutMilliseconds as i128),
|
||||
Value::Integer(timeout as i128),
|
||||
);
|
||||
Value::Map(sub_params)
|
||||
});
|
||||
|
||||
let response = self.send_bio_enrollment(
|
||||
Some(&pin_token),
|
||||
BioEnrollmentSubCommand::EnrollCaptureNextSample,
|
||||
next_sub_params,
|
||||
)?;
|
||||
|
||||
self.parse_bio_enrollment_response(&response)
|
||||
}
|
||||
|
||||
pub fn bio_enrollment_set_friendly_name(
|
||||
&self,
|
||||
pin: &str,
|
||||
template_id: &[u8],
|
||||
friendly_name: &str,
|
||||
) -> Result<(), PFError> {
|
||||
let pin_token = self.get_pin_token_with_permission(
|
||||
pin,
|
||||
PinUvAuthTokenPermissions::BIO_ENROLLMENT,
|
||||
None,
|
||||
)?;
|
||||
|
||||
let mut sub_params = BTreeMap::new();
|
||||
sub_params.insert(
|
||||
Value::Integer(BioEnrollmentSubParam::TemplateId as i128),
|
||||
Value::Bytes(template_id.to_vec()),
|
||||
);
|
||||
sub_params.insert(
|
||||
Value::Integer(BioEnrollmentSubParam::FriendlyName as i128),
|
||||
Value::Text(friendly_name.to_string()),
|
||||
);
|
||||
|
||||
self.send_bio_enrollment(
|
||||
Some(&pin_token),
|
||||
BioEnrollmentSubCommand::SetFriendlyName,
|
||||
Some(Value::Map(sub_params)),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn bio_enrollment_remove(&self, pin: &str, template_id: &[u8]) -> Result<(), PFError> {
|
||||
let pin_token = self.get_pin_token_with_permission(
|
||||
pin,
|
||||
PinUvAuthTokenPermissions::BIO_ENROLLMENT,
|
||||
None,
|
||||
)?;
|
||||
|
||||
let mut sub_params = BTreeMap::new();
|
||||
sub_params.insert(
|
||||
Value::Integer(BioEnrollmentSubParam::TemplateId as i128),
|
||||
Value::Bytes(template_id.to_vec()),
|
||||
);
|
||||
|
||||
self.send_bio_enrollment(
|
||||
Some(&pin_token),
|
||||
BioEnrollmentSubCommand::RemoveEnrollment,
|
||||
Some(Value::Map(sub_params)),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_bio_enrollment(
|
||||
&self,
|
||||
pin_token: Option<&[u8]>,
|
||||
sub_cmd: BioEnrollmentSubCommand,
|
||||
sub_params: Option<Value>,
|
||||
) -> Result<Value, PFError> {
|
||||
let sub_params_bytes = sub_params
|
||||
.as_ref()
|
||||
.map(|params| to_vec(params).map_err(|e| PFError::Io(e.to_string())))
|
||||
.transpose()?;
|
||||
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(
|
||||
Value::Integer(BioEnrollmentParam::Modality as i128),
|
||||
Value::Integer(1),
|
||||
);
|
||||
map.insert(
|
||||
Value::Integer(BioEnrollmentParam::SubCommand as i128),
|
||||
Value::Integer(sub_cmd as i128),
|
||||
);
|
||||
|
||||
if let Some(params) = sub_params {
|
||||
map.insert(
|
||||
Value::Integer(BioEnrollmentParam::SubCommandParams as i128),
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(token) = pin_token {
|
||||
map.insert(
|
||||
Value::Integer(BioEnrollmentParam::PinUvAuthProtocol as i128),
|
||||
Value::Integer(1),
|
||||
);
|
||||
map.insert(
|
||||
Value::Integer(BioEnrollmentParam::PinUvAuthParam as i128),
|
||||
Value::Bytes(
|
||||
self.sign_bio_enrollment_command(token, 1, sub_cmd as u8, sub_params_bytes.as_deref()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let mut payload = vec![CtapCommand::BioEnroll as u8];
|
||||
payload.extend(to_vec(&Value::Map(map)).map_err(|e| PFError::Io(e.to_string()))?);
|
||||
|
||||
let resp = self.send_cbor(CTAPHID_CBOR, &payload)?;
|
||||
from_slice(&resp).map_err(|e| PFError::Io(e.to_string()))
|
||||
}
|
||||
|
||||
fn sign_bio_enrollment_command(
|
||||
&self,
|
||||
pin_token: &[u8],
|
||||
modality: u8,
|
||||
sub_cmd: u8,
|
||||
sub_params_bytes: Option<&[u8]>,
|
||||
) -> Vec<u8> {
|
||||
let mut message = vec![modality, sub_cmd];
|
||||
if let Some(bytes) = sub_params_bytes {
|
||||
message.extend_from_slice(bytes);
|
||||
}
|
||||
|
||||
let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, pin_token);
|
||||
let sig = hmac::sign(&hmac_key, &message);
|
||||
sig.as_ref()[0..16].to_vec()
|
||||
}
|
||||
|
||||
fn parse_bio_enrollment_response(
|
||||
&self,
|
||||
response: &Value,
|
||||
) -> Result<BioEnrollmentResponse, PFError> {
|
||||
let map = match response {
|
||||
Value::Map(m) => m,
|
||||
_ => {
|
||||
return Err(PFError::Device(
|
||||
"Unexpected fingerprint enrollment response format".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let template_id =
|
||||
match map.get(&Value::Integer(BioEnrollmentResponseParam::TemplateId as i128)) {
|
||||
Some(Value::Bytes(bytes)) => Some(bytes.clone()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let status = match map.get(&Value::Integer(
|
||||
BioEnrollmentResponseParam::LastEnrollSampleStatus as i128,
|
||||
)) {
|
||||
Some(Value::Integer(v)) => Some(*v as u8),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let remaining_samples = match map.get(&Value::Integer(
|
||||
BioEnrollmentResponseParam::RemainingSamples as i128,
|
||||
)) {
|
||||
Some(Value::Integer(v)) => Some(*v as u32),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Ok(BioEnrollmentResponse {
|
||||
template_id,
|
||||
status,
|
||||
remaining_samples,
|
||||
})
|
||||
}
|
||||
|
||||
fn sign_credential_mgmt_command(
|
||||
&self,
|
||||
pin_token: &[u8],
|
||||
|
||||
+122
-3
@@ -3,15 +3,15 @@ pub mod hid;
|
||||
|
||||
use crate::{
|
||||
device::types::{
|
||||
AppConfig, AppConfigInput, DeviceInfo, DeviceMethod, FidoDeviceInfo, FullDeviceStatus,
|
||||
StoredCredential,
|
||||
AppConfig, AppConfigInput, DeviceInfo, DeviceMethod, FidoDeviceInfo,
|
||||
FingerprintEnrollResult, FingerprintStatus, FullDeviceStatus, StoredCredential,
|
||||
},
|
||||
error::PFError,
|
||||
};
|
||||
use constants::*;
|
||||
use hid::*;
|
||||
use serde_cbor_2::{Value, from_slice, to_vec};
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
// Fido functions that require pin:
|
||||
|
||||
@@ -367,6 +367,125 @@ pub(crate) fn set_min_pin_length(
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn get_fingerprint_status(pin: Option<String>) -> Result<FingerprintStatus, String> {
|
||||
let transport =
|
||||
HidTransport::open().map_err(|e| format!("Could not open HID transport: {}", e))?;
|
||||
|
||||
let info = get_fido_info()?;
|
||||
let options: HashMap<String, bool> = info.options.into_iter().collect();
|
||||
let supported = options.get("bioEnroll").copied().unwrap_or(false);
|
||||
let pin_configured = options.get("clientPin").copied().unwrap_or(false);
|
||||
let templates_loaded = pin.is_some();
|
||||
|
||||
if !supported {
|
||||
return Ok(FingerprintStatus {
|
||||
supported: false,
|
||||
pin_configured,
|
||||
templates_loaded: false,
|
||||
sensor: None,
|
||||
templates: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
let sensor = transport
|
||||
.bio_enrollment_get_fingerprint_sensor_info()
|
||||
.map_err(|e| format!("Failed to read fingerprint sensor info: {}", e))?;
|
||||
|
||||
let templates = if let Some(pin) = pin {
|
||||
transport
|
||||
.bio_enrollment_enumerate_enrollments(&pin)
|
||||
.map_err(|e| format!("Failed to enumerate fingerprints: {}", e))?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
Ok(FingerprintStatus {
|
||||
supported,
|
||||
pin_configured,
|
||||
templates_loaded,
|
||||
sensor: Some(sensor),
|
||||
templates,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn enroll_fingerprint(
|
||||
pin: String,
|
||||
timeout_ms: Option<u16>,
|
||||
) -> Result<FingerprintEnrollResult, String> {
|
||||
let transport =
|
||||
HidTransport::open().map_err(|e| format!("Could not open HID transport: {}", e))?;
|
||||
|
||||
let sensor = transport
|
||||
.bio_enrollment_get_fingerprint_sensor_info()
|
||||
.map_err(|e| format!("Failed to read fingerprint sensor info: {}", e))?;
|
||||
|
||||
let (template_id, mut step) = transport
|
||||
.bio_enrollment_begin(&pin, timeout_ms)
|
||||
.map_err(|e| format!("Failed to start fingerprint enrollment: {}", e))?;
|
||||
|
||||
let max_steps = sensor.max_capture_samples_required_for_enroll.max(1) + 4;
|
||||
let mut attempts = 0;
|
||||
|
||||
while step.remaining_samples.unwrap_or(0) > 0 && attempts < max_steps {
|
||||
step = transport
|
||||
.bio_enrollment_next(&pin, timeout_ms)
|
||||
.map_err(|e| format!("Fingerprint enrollment failed: {}", e))?;
|
||||
attempts += 1;
|
||||
}
|
||||
|
||||
let remaining_samples = step.remaining_samples.unwrap_or(0);
|
||||
let status = step.status.unwrap_or(0xff);
|
||||
|
||||
if remaining_samples > 0 {
|
||||
return Err(format!(
|
||||
"Fingerprint enrollment did not complete after {} capture steps",
|
||||
attempts
|
||||
));
|
||||
}
|
||||
|
||||
Ok(FingerprintEnrollResult {
|
||||
template_id: hex::encode_upper(template_id),
|
||||
status,
|
||||
message: if status == 0 {
|
||||
"Enrollment complete".to_string()
|
||||
} else {
|
||||
format!("Enrollment status 0x{:02X}", status)
|
||||
},
|
||||
remaining_samples,
|
||||
finished: remaining_samples == 0 && status == 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn rename_fingerprint(
|
||||
pin: String,
|
||||
template_id: String,
|
||||
friendly_name: String,
|
||||
) -> Result<String, String> {
|
||||
let transport =
|
||||
HidTransport::open().map_err(|e| format!("Could not open HID transport: {}", e))?;
|
||||
let template_id = hex::decode(&template_id)
|
||||
.map_err(|e| format!("Invalid template id '{}': {}", template_id, e))?;
|
||||
|
||||
transport
|
||||
.bio_enrollment_set_friendly_name(&pin, &template_id, &friendly_name)
|
||||
.map_err(|e| format!("Failed to update fingerprint name: {}", e))?;
|
||||
|
||||
Ok("Fingerprint name updated successfully".to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn remove_fingerprint(pin: String, template_id: String) -> Result<String, String> {
|
||||
let transport =
|
||||
HidTransport::open().map_err(|e| format!("Could not open HID transport: {}", e))?;
|
||||
let template_id = hex::decode(&template_id)
|
||||
.map_err(|e| format!("Invalid template id '{}': {}", template_id, e))?;
|
||||
|
||||
transport
|
||||
.bio_enrollment_remove(&pin, &template_id)
|
||||
.map_err(|e| format!("Failed to remove fingerprint: {}", e))?;
|
||||
|
||||
Ok("Fingerprint removed successfully".to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn get_credentials(pin: String) -> Result<Vec<StoredCredential>, String> {
|
||||
log::info!("Listing FIDO credentials via custom implementation...");
|
||||
|
||||
|
||||
@@ -47,6 +47,29 @@ pub(crate) fn set_min_pin_length(
|
||||
fido::set_min_pin_length(current_pin, min_pin_length)
|
||||
}
|
||||
|
||||
pub(crate) fn get_fingerprint_status(pin: Option<String>) -> Result<FingerprintStatus, String> {
|
||||
fido::get_fingerprint_status(pin)
|
||||
}
|
||||
|
||||
pub(crate) fn enroll_fingerprint(
|
||||
pin: String,
|
||||
timeout_ms: Option<u16>,
|
||||
) -> Result<FingerprintEnrollResult, String> {
|
||||
fido::enroll_fingerprint(pin, timeout_ms)
|
||||
}
|
||||
|
||||
pub(crate) fn rename_fingerprint(
|
||||
pin: String,
|
||||
template_id: String,
|
||||
friendly_name: String,
|
||||
) -> Result<String, String> {
|
||||
fido::rename_fingerprint(pin, template_id, friendly_name)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_fingerprint(pin: String, template_id: String) -> Result<String, String> {
|
||||
fido::remove_fingerprint(pin, template_id)
|
||||
}
|
||||
|
||||
pub fn reboot(to_bootsel: bool) -> Result<String, PFError> {
|
||||
rescue::reboot_device(to_bootsel)
|
||||
}
|
||||
|
||||
@@ -101,3 +101,39 @@ pub struct StoredCredential {
|
||||
pub user_id: String,
|
||||
pub credential_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FingerprintTemplate {
|
||||
pub template_id: String,
|
||||
pub friendly_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FingerprintSensorInfo {
|
||||
pub modality: String,
|
||||
pub fingerprint_kind: String,
|
||||
pub max_capture_samples_required_for_enroll: u32,
|
||||
pub max_template_friendly_name: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FingerprintStatus {
|
||||
pub supported: bool,
|
||||
pub pin_configured: bool,
|
||||
pub templates_loaded: bool,
|
||||
pub sensor: Option<FingerprintSensorInfo>,
|
||||
pub templates: Vec<FingerprintTemplate>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FingerprintEnrollResult {
|
||||
pub template_id: String,
|
||||
pub status: u8,
|
||||
pub message: String,
|
||||
pub remaining_samples: u32,
|
||||
pub finished: bool,
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use gpui_component::{
|
||||
};
|
||||
|
||||
type PinPromptCallback = std::rc::Rc<dyn Fn(String, WeakEntity<PinPromptContent>, &mut App)>;
|
||||
type TextPromptCallback = std::rc::Rc<dyn Fn(String, WeakEntity<TextPromptContent>, &mut App)>;
|
||||
type ConfirmCallback = std::rc::Rc<dyn Fn(WeakEntity<ConfirmContent>, &mut App)>;
|
||||
type ChangePinCallback =
|
||||
std::rc::Rc<dyn Fn(String, String, WeakEntity<ChangePinContent>, &mut App)>;
|
||||
@@ -244,6 +245,236 @@ pub fn open_pin_prompt(
|
||||
});
|
||||
}
|
||||
|
||||
pub struct TextPromptContent {
|
||||
phase: DialogPhase,
|
||||
title: SharedString,
|
||||
description: SharedString,
|
||||
confirm_label: SharedString,
|
||||
text_input: Entity<InputState>,
|
||||
on_confirm: TextPromptCallback,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl TextPromptContent {
|
||||
fn set_loading(&mut self, cx: &mut Context<Self>) {
|
||||
self.phase = DialogPhase::Loading;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_success(&mut self, msg: String, cx: &mut Context<Self>) {
|
||||
self.phase = DialogPhase::Success(msg);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_error(&mut self, msg: String, cx: &mut Context<Self>) {
|
||||
self.phase = DialogPhase::Error(msg);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn trigger_confirm(&mut self, cx: &mut Context<Self>) {
|
||||
if matches!(self.phase, DialogPhase::Loading | DialogPhase::Success(_)) {
|
||||
return;
|
||||
}
|
||||
let text = self.text_input.read(cx).text().to_string();
|
||||
let text = text.trim().to_string();
|
||||
if !text.is_empty() {
|
||||
let handle = cx.entity().downgrade();
|
||||
self.set_loading(cx);
|
||||
(self.on_confirm)(text, handle, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for TextPromptContent {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let phase = self.phase.clone();
|
||||
|
||||
match &phase {
|
||||
DialogPhase::Success(msg) => v_flex()
|
||||
.gap_4()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.child(
|
||||
gpui_component::Icon::new(gpui_component::IconName::CircleCheck)
|
||||
.text_color(cx.theme().green)
|
||||
.with_size(gpui_component::Size::Large),
|
||||
)
|
||||
.child(self.title.clone()),
|
||||
)
|
||||
.child(msg.clone())
|
||||
.child(
|
||||
h_flex().justify_end().child(
|
||||
Button::new("done")
|
||||
.primary()
|
||||
.label("Done")
|
||||
.on_click(|_, window, cx| {
|
||||
window.close_dialog(cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.into_any_element(),
|
||||
|
||||
DialogPhase::Loading => v_flex()
|
||||
.gap_4()
|
||||
.child(self.description.clone())
|
||||
.child(Input::new(&self.text_input).disabled(true))
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.gap_2()
|
||||
.child(Button::new("cancel").label("Cancel").disabled(true))
|
||||
.child(
|
||||
Button::new("confirm")
|
||||
.primary()
|
||||
.label("Loading...")
|
||||
.loading(true),
|
||||
),
|
||||
)
|
||||
.into_any_element(),
|
||||
|
||||
DialogPhase::Error(err_msg) => {
|
||||
let text_input = self.text_input.clone();
|
||||
let confirm_label = self.confirm_label.clone();
|
||||
let on_confirm = self.on_confirm.clone();
|
||||
let handle = cx.entity().downgrade();
|
||||
|
||||
v_flex()
|
||||
.gap_4()
|
||||
.child(self.description.clone())
|
||||
.child(
|
||||
div()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.rounded_md()
|
||||
.bg(rgb(0x18181b))
|
||||
.text_color(rgb(0xef4444))
|
||||
.text_sm()
|
||||
.child(render_error_message(err_msg.clone())),
|
||||
)
|
||||
.child(Input::new(&text_input))
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.gap_2()
|
||||
.child(Button::new("cancel").label("Cancel").on_click(
|
||||
|_, window, cx| {
|
||||
window.close_dialog(cx);
|
||||
},
|
||||
))
|
||||
.child(
|
||||
Button::new("confirm")
|
||||
.primary()
|
||||
.label(confirm_label)
|
||||
.on_click(move |_, _, cx| {
|
||||
let text = text_input.read(cx).text().to_string();
|
||||
let text = text.trim().to_string();
|
||||
if !text.is_empty() {
|
||||
if let Some(h) = handle.upgrade() {
|
||||
h.update(cx, |this, cx| this.set_loading(cx));
|
||||
}
|
||||
on_confirm(text, handle.clone(), cx);
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
DialogPhase::Input => {
|
||||
let text_input = self.text_input.clone();
|
||||
let confirm_label = self.confirm_label.clone();
|
||||
let on_confirm = self.on_confirm.clone();
|
||||
let handle = cx.entity().downgrade();
|
||||
|
||||
v_flex()
|
||||
.gap_4()
|
||||
.child(self.description.clone())
|
||||
.child(Input::new(&text_input))
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.gap_2()
|
||||
.child(Button::new("cancel").label("Cancel").on_click(
|
||||
|_, window, cx| {
|
||||
window.close_dialog(cx);
|
||||
},
|
||||
))
|
||||
.child(
|
||||
Button::new("confirm")
|
||||
.primary()
|
||||
.label(confirm_label)
|
||||
.on_click(move |_, _, cx| {
|
||||
let text = text_input.read(cx).text().to_string();
|
||||
let text = text.trim().to_string();
|
||||
if !text.is_empty() {
|
||||
if let Some(h) = handle.upgrade() {
|
||||
h.update(cx, |this, cx| this.set_loading(cx));
|
||||
}
|
||||
on_confirm(text, handle.clone(), cx);
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_text_prompt(
|
||||
title: &str,
|
||||
description: &str,
|
||||
placeholder: &str,
|
||||
confirm_label: &str,
|
||||
initial_value: Option<String>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
on_confirm: impl Fn(String, WeakEntity<TextPromptContent>, &mut App) + 'static,
|
||||
) {
|
||||
let title_str = SharedString::from(title.to_string());
|
||||
let description = SharedString::from(description.to_string());
|
||||
let confirm_label = SharedString::from(confirm_label.to_string());
|
||||
|
||||
let initial_text = initial_value.unwrap_or_default();
|
||||
let placeholder = SharedString::from(placeholder.to_string());
|
||||
let text_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder(placeholder.clone())
|
||||
.default_value(initial_text.clone())
|
||||
});
|
||||
|
||||
let dialog_title = title_str.clone();
|
||||
let text_for_sub = text_input.clone();
|
||||
|
||||
let content = cx.new(|cx| {
|
||||
let sub = cx.subscribe(&text_for_sub, |this: &mut TextPromptContent, _, event, cx| {
|
||||
if matches!(event, InputEvent::PressEnter { .. }) {
|
||||
this.trigger_confirm(cx);
|
||||
}
|
||||
});
|
||||
|
||||
TextPromptContent {
|
||||
phase: DialogPhase::Input,
|
||||
title: title_str,
|
||||
description,
|
||||
confirm_label,
|
||||
text_input: text_for_sub,
|
||||
on_confirm: std::rc::Rc::new(on_confirm),
|
||||
_subscription: sub,
|
||||
}
|
||||
});
|
||||
|
||||
window.open_dialog(cx, move |dialog, _, _| {
|
||||
dialog
|
||||
.title(dialog_title.clone())
|
||||
.child(content.clone())
|
||||
.overlay_closable(false)
|
||||
.close_button(false)
|
||||
});
|
||||
}
|
||||
|
||||
pub struct ConfirmContent {
|
||||
phase: DialogPhase,
|
||||
title: SharedString,
|
||||
|
||||
+12
-1
@@ -60,6 +60,7 @@ impl ApplicationRoot {
|
||||
|
||||
if device_changed {
|
||||
self.views.passkeys = None;
|
||||
self.views.security = None;
|
||||
}
|
||||
|
||||
match io::get_fido_info() {
|
||||
@@ -172,7 +173,17 @@ impl Render for ApplicationRoot {
|
||||
}
|
||||
self.views.config.clone().unwrap().into_any_element()
|
||||
}
|
||||
ActiveView::Security => SecurityView::build(cx).into_any_element(),
|
||||
ActiveView::Security => {
|
||||
if self.views.security.is_none() {
|
||||
let root = cx.entity().downgrade();
|
||||
let view = cx.new(|cx| SecurityView::new(window, cx, root));
|
||||
view.update(cx, |view, cx| {
|
||||
view.refresh_status(None, cx);
|
||||
});
|
||||
self.views.security = Some(view);
|
||||
}
|
||||
self.views.security.clone().unwrap().into_any_element()
|
||||
}
|
||||
ActiveView::About => AboutView::build(cx.theme()).into_any_element(),
|
||||
});
|
||||
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
device::types::{FidoDeviceInfo, FullDeviceStatus},
|
||||
ui::views::{config::ConfigView, passkeys::PasskeysView},
|
||||
ui::views::{config::ConfigView, passkeys::PasskeysView, security::SecurityView},
|
||||
};
|
||||
use gpui::{Entity, Pixels, SharedString, px};
|
||||
|
||||
@@ -54,6 +54,7 @@ impl LayoutState {
|
||||
pub struct ViewCache {
|
||||
pub passkeys: Option<Entity<PasskeysView>>,
|
||||
pub config: Option<Entity<ConfigView>>,
|
||||
pub security: Option<Entity<SecurityView>>,
|
||||
}
|
||||
|
||||
impl ViewCache {
|
||||
@@ -61,6 +62,7 @@ impl ViewCache {
|
||||
Self {
|
||||
passkeys: None,
|
||||
config: None,
|
||||
security: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+807
-152
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user