@@ -125,6 +125,7 @@ struct SuggestionItem {
125125 title : String ,
126126 username_hint : Option < String > ,
127127 match_strength : u8 ,
128+ credential_type : String ,
128129}
129130
130131#[ derive( Debug , Serialize ) ]
@@ -157,6 +158,9 @@ struct FillResponse {
157158struct TotpPayload {
158159 origin : String ,
159160 item_id : String ,
161+ /// Indicates this request was triggered by an explicit user action (click, keyboard).
162+ #[ serde( default ) ]
163+ user_gesture : bool ,
160164}
161165
162166#[ derive( Debug , Serialize ) ]
@@ -313,7 +317,7 @@ async fn handle_request(
313317 let parsed: SuggestionsPayload = serde_json:: from_value ( req. payload )
314318 . context ( "invalid payload for get_suggestions" ) ?;
315319 let host = origin_to_host ( & parsed. origin ) ?;
316- let items = get_password_suggestions ( db_path, & host) . await ?;
320+ let items = get_credential_suggestions ( db_path, & host) . await ?;
317321 let payload = serde_json:: to_value ( SuggestionsResponse {
318322 items,
319323 suggesting_for : host,
@@ -425,6 +429,18 @@ async fn handle_request(
425429 serde_json:: from_value ( req. payload ) . context ( "invalid payload for get_totp" ) ?;
426430 let host = origin_to_host ( & parsed. origin ) ?;
427431
432+ let require_gesture = std:: env:: var ( "PERSONA_BRIDGE_REQUIRE_GESTURE" )
433+ . map ( |v| v != "0" && v. to_lowercase ( ) != "false" )
434+ . unwrap_or ( true ) ;
435+ if require_gesture && !parsed. user_gesture {
436+ warn ! (
437+ origin = %parsed. origin,
438+ item_id = %parsed. item_id,
439+ "totp request rejected: user_gesture required but not provided"
440+ ) ;
441+ return Err ( anyhow ! ( "user_gesture_required: totp must be triggered by explicit user action" ) ) ;
442+ }
443+
428444 let master_password = std:: env:: var ( "PERSONA_MASTER_PASSWORD" )
429445 . ok ( )
430446 . filter ( |s| !s. trim ( ) . is_empty ( ) )
@@ -450,6 +466,10 @@ async fn handle_request(
450466 return Err ( anyhow ! ( "unsupported_credential_type" ) ) ;
451467 }
452468
469+ if cred. url . is_none ( ) {
470+ return Err ( anyhow ! ( "origin_binding_required: totp entries must have a URL set" ) ) ;
471+ }
472+
453473 if !validate_origin_binding ( & host, cred. url . as_deref ( ) ) {
454474 warn ! (
455475 origin = %parsed. origin,
@@ -1048,7 +1068,7 @@ async fn compute_status(db_path: &PathBuf) -> Result<(bool, Option<String>)> {
10481068 Ok ( ( locked, active_identity) )
10491069}
10501070
1051- async fn get_password_suggestions ( db_path : & PathBuf , host : & str ) -> Result < Vec < SuggestionItem > > {
1071+ async fn get_credential_suggestions ( db_path : & PathBuf , host : & str ) -> Result < Vec < SuggestionItem > > {
10521072 let db = open_db ( db_path) . await ?;
10531073 let repo = CredentialRepository :: new ( db) ;
10541074 let all = repo. find_all ( ) . await ?;
@@ -1058,15 +1078,18 @@ async fn get_password_suggestions(db_path: &PathBuf, host: &str) -> Result<Vec<S
10581078 if !cred. is_active {
10591079 continue ;
10601080 }
1061- if cred. credential_type != CredentialType :: Password {
1081+ let kind = match cred. credential_type {
1082+ CredentialType :: Password => "password" ,
1083+ CredentialType :: TwoFactor => "totp" ,
1084+ _ => continue ,
1085+ } ;
1086+
1087+ if cred. url . is_none ( ) {
10621088 continue ;
10631089 }
10641090
10651091 // Calculate match strength based on URL similarity.
1066- let match_strength = match cred. url . as_deref ( ) {
1067- Some ( url) => compute_match_strength ( host, url) ,
1068- None => 0 ,
1069- } ;
1092+ let match_strength = compute_match_strength ( host, cred. url . as_deref ( ) . unwrap_or_default ( ) ) ;
10701093
10711094 if match_strength == 0 {
10721095 continue ;
@@ -1077,6 +1100,7 @@ async fn get_password_suggestions(db_path: &PathBuf, host: &str) -> Result<Vec<S
10771100 title : cred. name ,
10781101 username_hint : cred. username ,
10791102 match_strength,
1103+ credential_type : kind. to_string ( ) ,
10801104 } ) ;
10811105 }
10821106
0 commit comments