Passkeys for the browser extension, tags in item lists
The browser bridge answers passkey_query/get/create/result/cancel: the page's RP ID is checked against the tab origin (no public suffixes), every request waits for the user to approve it in the app, and the extension polls for the result. Tags are normalised on save and included in item summaries. confirm_master_password verifies the user where there are no biometrics. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
|
||||
pub mod broker;
|
||||
pub mod channel;
|
||||
pub mod passkey;
|
||||
pub mod words;
|
||||
|
||||
use std::path::PathBuf;
|
||||
@@ -41,6 +42,31 @@ pub struct BrowserSaveRequest {
|
||||
pub update: bool,
|
||||
}
|
||||
|
||||
/// One passkey the user can pick for a sign-in.
|
||||
#[derive(Debug, Clone, uniffi::Record)]
|
||||
pub struct BrowserPasskeyChoice {
|
||||
/// base64url; pass it back to `resolve_passkey`.
|
||||
pub credential_id: String,
|
||||
pub user_name: String,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
/// A page in the browser asks to sign in with a passkey (`create` false) or to save a new one (`create` true).
|
||||
/// Show it, **verify the user** (Touch ID, Windows Hello or the master password), then `resolve_passkey`.
|
||||
#[derive(Debug, Clone, uniffi::Record)]
|
||||
pub struct BrowserPasskeyRequest {
|
||||
pub id: String,
|
||||
pub create: bool,
|
||||
/// The page, e.g. `https://accounts.google.com`.
|
||||
pub origin: String,
|
||||
pub rp_id: String,
|
||||
/// Create only: the site's name and the account name the site gave.
|
||||
pub rp_name: String,
|
||||
pub user_name: String,
|
||||
/// Sign-in only: the passkeys to choose from.
|
||||
pub choices: Vec<BrowserPasskeyChoice>,
|
||||
}
|
||||
|
||||
/// A paired browser, for the settings list.
|
||||
#[derive(Debug, Clone, uniffi::Record)]
|
||||
pub struct BrowserPairing {
|
||||
@@ -57,6 +83,9 @@ pub struct BrowserPairing {
|
||||
pub trait BrowserBridgeUi: Send + Sync {
|
||||
fn pairing_requested(&self, request: BrowserPairingRequest);
|
||||
fn save_requested(&self, request: BrowserSaveRequest);
|
||||
fn passkey_requested(&self, request: BrowserPasskeyRequest);
|
||||
/// The page stopped waiting for that passkey request (closed, navigated away, timed out): hide it.
|
||||
fn passkey_settled(&self, id: String);
|
||||
/// A fill or query arrived: counts as activity for auto-lock.
|
||||
fn activity(&self);
|
||||
}
|
||||
@@ -70,11 +99,33 @@ impl Ui for UiAdapter {
|
||||
fn save_requested(&self, req: &SaveView) {
|
||||
self.0.save_requested(save_request(req.clone()));
|
||||
}
|
||||
fn passkey_requested(&self, req: &passkey::PasskeyView) {
|
||||
self.0.passkey_requested(passkey_request(req.clone()));
|
||||
}
|
||||
fn passkey_settled(&self, id: &str) {
|
||||
self.0.passkey_settled(id.to_string());
|
||||
}
|
||||
fn activity(&self) {
|
||||
self.0.activity();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn passkey_request(v: passkey::PasskeyView) -> BrowserPasskeyRequest {
|
||||
BrowserPasskeyRequest {
|
||||
id: v.id,
|
||||
create: v.kind == passkey::PasskeyKind::Create,
|
||||
origin: v.origin,
|
||||
rp_id: v.rp_id,
|
||||
rp_name: v.rp_name,
|
||||
user_name: v.user_name,
|
||||
choices: v
|
||||
.choices
|
||||
.into_iter()
|
||||
.map(|c| BrowserPasskeyChoice { credential_id: c.credential_id, user_name: c.user_name, title: c.title })
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn pairing_request(v: PairingView) -> BrowserPairingRequest {
|
||||
BrowserPairingRequest { id: v.id, name: v.name, caller: v.caller, words: v.words }
|
||||
}
|
||||
@@ -172,6 +223,16 @@ impl BrowserBridge {
|
||||
pub fn resolve_save(&self, id: String, accept: bool) -> Result<Option<String>, CoreError> {
|
||||
self.broker.resolve_save(&id, accept).map_err(invalid)
|
||||
}
|
||||
|
||||
pub fn pending_passkeys(&self) -> Vec<BrowserPasskeyRequest> {
|
||||
self.broker.pending_passkeys().into_iter().map(passkey_request).collect()
|
||||
}
|
||||
|
||||
/// Answers a passkey request. **Verify the user right before accepting** (docs/05 "User verification").
|
||||
/// `credential_id` picks the passkey for a sign-in (`None`: the first choice).
|
||||
pub fn resolve_passkey(&self, id: String, accept: bool, credential_id: Option<String>) -> Result<(), CoreError> {
|
||||
self.broker.resolve_passkey(&id, accept, credential_id.as_deref()).map_err(invalid)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for BrowserBridge {
|
||||
@@ -190,6 +251,8 @@ mod tests {
|
||||
impl BrowserBridgeUi for NoUi {
|
||||
fn pairing_requested(&self, _: BrowserPairingRequest) {}
|
||||
fn save_requested(&self, _: BrowserSaveRequest) {}
|
||||
fn passkey_requested(&self, _: BrowserPasskeyRequest) {}
|
||||
fn passkey_settled(&self, _: String) {}
|
||||
fn activity(&self) {}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user