Skip to content
40 changes: 36 additions & 4 deletions credentialsd-common/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub const BACKGROUND_EVENT_ERROR_PIN_NOT_SET: u32 = 0x80000008;
pub enum BackgroundEvent {
CeremonyCompleted,
NeedsPin { attempts_left: Option<u32> },
PinNotSet { error: PinNotSetError },
NeedsUserVerification { attempts_left: Option<u32> },
NeedsUserPresence,
SelectingCredential { creds: Vec<Credential> },
Expand Down Expand Up @@ -51,6 +52,11 @@ pub enum BackgroundEvent {
#[zvariant(signature = "dict")]
pub struct ClientPinEnteredOptions {}

/// Emitted when a client enters a new PIN for a device.
#[derive(Debug, SerializeDict, DeserializeDict, PartialEq, Type)]
#[zvariant(signature = "dict")]
pub struct SetDevicePinOptions {}

#[derive(Clone, Debug, Default, SerializeDict, DeserializeDict, PartialEq, Type, Value)]
#[zvariant(signature = "dict")]
pub struct Credential {
Expand Down Expand Up @@ -81,6 +87,24 @@ impl From<DiscoveryRequestedOptions> for UserInteractedEvent {
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)]
#[zvariant(signature = "s")]
#[serde(rename_all = "PascalCase")]
pub enum PinNotSetError {
/// PIN too short
PinTooShort,
/// PIN too long
PinTooLong,
/// PIN violates PinPolicy
PinPolicyViolation,
/// PIN change is required by the device
PinChangeRequired,
/// When no specific error is given (happens either in the initial PinNotSet-iteration,
/// or if dbus receives an unknown string)
#[serde(other)]
PinNotSet,
}

#[derive(Debug, Clone)]
pub enum Error {
/// Some unknown error with the authenticator occurred.
Expand All @@ -95,8 +119,6 @@ pub enum Error {
/// Note that this is different than exhausting the PIN count that fully
/// locks out the device.
PinAttemptsExhausted,
/// The RP requires user verification, but the device has no PIN/Biometrics set.
PinNotSet,
// TODO: We may want to hide the details on this variant from the public API.
/// Something went wrong with the credential service itself, not the authenticator.
Internal(String),
Expand All @@ -108,7 +130,6 @@ impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AuthenticatorError => f.write_str("AuthenticatorError"),
Self::PinNotSet => f.write_str("PinNotSet"),
Self::NoCredentials => f.write_str("NoCredentials"),
Self::CredentialExcluded => f.write_str("CredentialExcluded"),
Self::PinAttemptsExhausted => f.write_str("PinAttemptsExhausted"),
Expand All @@ -124,7 +145,6 @@ impl TryFrom<&Value<'_>> for Error {
let err_code: &str = value.downcast_ref()?;
let err = match err_code {
"AuthenticatorError" => crate::model::Error::AuthenticatorError,
"PinNotSet" => crate::model::Error::PinNotSet,
"NoCredentials" => crate::model::Error::NoCredentials,
"CredentialExcluded" => crate::model::Error::CredentialExcluded,
"PinAttemptsExhausted" => crate::model::Error::PinAttemptsExhausted,
Expand All @@ -138,6 +158,10 @@ impl TryFrom<&Value<'_>> for Error {
#[zvariant(signature = "dict")]
pub struct NotifyNeedsPinOptions {}

#[derive(Debug, PartialEq, SerializeDict, DeserializeDict, Type)]
#[zvariant(signature = "dict")]
pub struct NotifyPinNotSetOptions {}

#[derive(Debug, SerializeDict, DeserializeDict, Type)]
#[zvariant(signature = "dict")]
pub struct NotifyNeedsUserVerificationOptions {}
Expand Down Expand Up @@ -253,6 +277,10 @@ pub enum UserInteractedEvent {
/// File descriptor must be memory-mapped to be read.
ClientPinEntered(OwnedFd),

/// Set new client PIN. Length of the PIN MUST not be greater than 63 bytes.
/// File descriptor must be memory-mapped to be read.
SetDevicePin(OwnedFd),

/// Select a credential by credential ID
CredentialSelected(String),

Expand All @@ -267,6 +295,10 @@ impl std::fmt::Debug for UserInteractedEvent {
.debug_tuple(stringify!(ClientPinEntered))
.field(&"******".to_string())
.finish(),
Self::SetDevicePin(_) => f
.debug_tuple(stringify!(SetDevicePin))
.field(&"******".to_string())
.finish(),
Self::CredentialSelected(arg0) => f
.debug_tuple(stringify!(CredentialSelected))
.field(arg0)
Expand Down
60 changes: 60 additions & 0 deletions credentialsd-ui/data/resources/ui/window.blp
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,51 @@ template $CredentialsUiWindow: ApplicationWindow {
};
}

StackPage {
name: "set_new_pin";
title: _("Set a PIN");

child: Box {
orientation: vertical;

Label {
label: _("Please choose a new PIN for your device.");
wrap: true;
}

PasswordEntry new_pin_primary_entry {
placeholder-text: _("New PIN");
changed => $handle_setting_pin_change() swapped;
}

PasswordEntry new_pin_confirm_entry {
placeholder-text: _("Confirm PIN");
changed => $handle_setting_pin_change() swapped;
}

Box {
halign: end;
spacing: 6;

Button new_pin_btn_close_window {
label: _("Close");
clicked => $handle_close_window() swapped;
}

Button new_pin_btn_continue {
label: _("Continue");
sensitive: bind template.view-model as <$CredentialManagerViewModel>.pin_fields_match;

styles [
"suggested-action"
]

clicked => $handle_commit_new_pin() swapped;
}
}
};
}

StackPage {
name: "completed";
title: _("Complete");
Expand Down Expand Up @@ -305,7 +350,22 @@ template $CredentialsUiWindow: ApplicationWindow {
Label {
label: bind template.view-model as <$CredentialManagerViewModel>.prompt;
}

Box {
halign: end;
spacing: 6;
visible: bind template.view-model as <$CredentialManagerViewModel>.start_setting_new_pin_visible;
Button failed_close_window {
label: _("Close");
clicked => $handle_close_window() swapped;
}
Button start_setting_new_pin {
label: _("Set PIN on device");
clicked => $handle_start_setting_new_pin() swapped;
}
}
};

}
}
}
Expand Down
1 change: 0 additions & 1 deletion credentialsd-ui/po/de_DE.po
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ msgstr "Scannen Sie den QR-Code, um Ihr Gerät zu verbinden"
#: credentialsd-ui/data/resources/ui/window.blp:253
msgid "Choose credential"
msgstr "Wählen Sie Zugangsdaten aus"

#: credentialsd-ui/data/resources/ui/window.blp:266
msgid "Complete"
msgstr "Abgeschlossen"
Expand Down
21 changes: 20 additions & 1 deletion credentialsd-ui/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use async_std::{

use credentialsd_common::{
memfd::write_secret,
model::{BackgroundEvent, UserInteractedEvent},
model::{BackgroundEvent, PinNotSetError, UserInteractedEvent},
};

const CTAP_CLIENT_SECRET_MAX_LEN: usize = 63;
Expand Down Expand Up @@ -39,6 +39,25 @@ impl FlowControlClient {
.await
}

pub async fn set_device_pin(&mut self, pin: String) -> Result<(), Option<PinNotSetError>> {
if pin.len() > CTAP_CLIENT_SECRET_MAX_LEN {
tracing::warn!("PIN is too long");
return Err(Some(PinNotSetError::PinTooLong));
}
let fd = match write_secret(pin.into_bytes()) {
Ok(fd) => fd,
Err(err) => {
tracing::error!(%err, "Failed to write secret to file descriptor");
// TODO: need to send a message back to GUI thread that there was an error.
_ = self.cancel_request().await;
return Err(None);
}
};
self.send(UserInteractedEvent::SetDevicePin(fd.into()))
.await
.map_err(|_| None)
}

pub async fn select_credential(&self, credential_id: String) -> Result<(), ()> {
self.send(UserInteractedEvent::CredentialSelected(credential_id))
.await
Expand Down
36 changes: 33 additions & 3 deletions credentialsd-ui/src/dbus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ use credentialsd_common::model::{
ClientPinEnteredOptions, Credential, CredentialSelectedOptions, Device,
DiscoveryRequestedOptions, NotifyHybridConnectedOptions, NotifyHybridConnectingOptions,
NotifyHybridStartedOptions, NotifyNeedsPinOptions, NotifyNeedsUserPresenceOptions,
NotifyNeedsUserVerificationOptions, NotifyNfcConnectedOptions,
NotifySelectingCredentialOptions, NotifyUsbConnectedOptions, Operation, PortalBackendOptions,
UserInteractedEvent, WindowHandle,
NotifyNeedsUserVerificationOptions, NotifyNfcConnectedOptions, NotifyPinNotSetOptions,
NotifySelectingCredentialOptions, NotifyUsbConnectedOptions, Operation, PinNotSetError,
PortalBackendOptions, SetDevicePinOptions, UserInteractedEvent, WindowHandle,
};

use crate::{RequestingApplication, ViewRequest, client::FlowControlClient};
Expand Down Expand Up @@ -168,6 +168,23 @@ impl CredentialPortalBackend {
.await
}

/// Called when the authenticator needs a client PIN, but the device
/// has no PIN set yet. This flow allows to set a new PIN on the fly.
async fn notify_pin_not_set(
&self,
#[zbus(object_server)] object_server: &ObjectServer,
session_handle: ObjectPath<'_>,
error: PinNotSetError,
_options: NotifyPinNotSetOptions,
) -> fdo::Result<()> {
self.notify_state_changed(
object_server,
session_handle,
BackgroundEvent::PinNotSet { error },
)
.await
}

/// Called when the authenticator needs a user verification gesture.
async fn notify_needs_user_verification(
&self,
Expand Down Expand Up @@ -345,6 +362,14 @@ impl CredentialPortalBackend {
options: ClientPinEnteredOptions,
) -> zbus::Result<()>;

#[zbus(signal)]
async fn set_device_pin(
emitter: SignalEmitter<'_>,
session_handle: ObjectPath<'_>,
pin_fd: OwnedFd,
options: SetDevicePinOptions,
) -> zbus::Result<()>;

#[zbus(signal)]
async fn credential_selected(
emitter: SignalEmitter<'_>,
Expand Down Expand Up @@ -621,6 +646,11 @@ impl CeremonyObject {
.client_pin_entered(session_handle, pin_fd, ClientPinEnteredOptions {})
.await?;
}
UserInteractedEvent::SetDevicePin(pin_fd) => {
emitter
.set_device_pin(session_handle, pin_fd, SetDevicePinOptions {})
.await?;
}
UserInteractedEvent::CredentialSelected(id) => {
emitter
.credential_selected(session_handle, id, CredentialSelectedOptions {})
Expand Down
5 changes: 4 additions & 1 deletion credentialsd-ui/src/gui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::{sync::Arc, thread::JoinHandle};
use async_std::{channel::Receiver, sync::Mutex as AsyncMutex};

use credentialsd_common::model::Device;
use credentialsd_common::model::{Credential, WindowHandle};
use credentialsd_common::model::{Credential, PinNotSetError, WindowHandle};

use crate::{ViewRequest, client::FlowControlClient};

Expand Down Expand Up @@ -83,6 +83,9 @@ pub enum ViewUpdate {
NeedsPin {
attempts_left: Option<u32>,
},
PinNotSet {
error: PinNotSetError,
},
NeedsUserVerification {
attempts_left: Option<u32>,
},
Expand Down
39 changes: 36 additions & 3 deletions credentialsd-ui/src/gui/view_model/gtk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pub mod device;
mod window;

use async_std::channel::{Receiver, Sender};
use credentialsd_common::model::WindowHandle;
use credentialsd_common::model::{PinNotSetError, WindowHandle};
use gettextrs::{LocaleCategory, gettext, ngettext};
use glib::clone;
use gtk::gdk::Texture;
Expand Down Expand Up @@ -83,6 +83,12 @@ mod imp {

#[property(get, set)]
pub qr_spinner_visible: RefCell<bool>,

#[property(get, set)]
pub start_setting_new_pin_visible: RefCell<bool>,

#[property(get, set)]
pub pin_fields_match: RefCell<bool>,
}

// The central trait for subclassing a GObject
Expand Down Expand Up @@ -131,6 +137,8 @@ impl ViewModel {
Ok(update) => {
// TODO: hack so I don't have to unset this in every event manually.
view_model.set_usb_nfc_pin_entry_visible(false);
view_model.set_start_setting_new_pin_visible(false);
view_model.set_failed(false);
match update {
ViewUpdate::SetTitle {
title,
Expand Down Expand Up @@ -184,6 +192,27 @@ impl ViewModel {
ViewUpdate::NeedsUserPresence => {
view_model.set_prompt(gettext("Touch your device"));
}
ViewUpdate::PinNotSet { error } => {
view_model.set_failed(true);
view_model.set_start_setting_new_pin_visible(true);
let text = match error {
PinNotSetError::PinNotSet => gettext(
"This server requires your device to have additional protection like a PIN, which is not set. Please set a PIN for this device and try again.",
),
PinNotSetError::PinTooShort
| PinNotSetError::PinPolicyViolation => gettext(
"The entered PIN violates the PIN-policy of this device (likely too short). Please try again.",
),
PinNotSetError::PinTooLong => gettext(
"The entered PIN violates the PIN-policy of this device (PIN too long). Please try again.",
),
PinNotSetError::PinChangeRequired => gettext(
"A PIN change is required by your device to continue.",
),
};
// These are already gettext messages
view_model.set_prompt(text);
}
ViewUpdate::HybridNeedsQrCode(qr_code) => {
view_model.set_prompt(gettext("Scan the QR code with your device to begin authentication."));
let texture = view_model.draw_qr_code(&qr_code);
Expand Down Expand Up @@ -214,11 +243,11 @@ impl ViewModel {
view_model.set_qr_spinner_visible(false);
view_model.set_completed(true);
}
ViewUpdate::Failed(error_msg) => {
ViewUpdate::Failed(error) => {
view_model.set_qr_spinner_visible(false);
view_model.set_failed(true);
// These are already gettext messages
view_model.set_prompt(error_msg);
view_model.set_prompt(error);
}
ViewUpdate::Cancelled => {
view_model.set_state(ModelState::Cancelled)
Expand Down Expand Up @@ -322,6 +351,10 @@ impl ViewModel {
self.send_event(ViewEvent::PinEntered(pin)).await;
}

pub async fn send_set_new_device_pin(&self, pin: String) {
self.send_event(ViewEvent::SetNewDevicePin(pin)).await;
}

fn draw_qr_code(&self, qr_data: &str) -> Texture {
let qr_code = QrCode::new(qr_data).expect("QR code to be valid");
let svg_xml = qr_code.render::<qrcode::render::svg::Color>().build();
Expand Down
Loading