@@ -15,7 +15,9 @@ use zeroize::Zeroizing;
1515
1616use vault_api:: BitwardenClient ;
1717use vault_core:: EncString ;
18- use vault_core:: cipher:: { Card , Cipher , DecryptOptions , Login , LoginUri , PlainCard , PlainCipher } ;
18+ use vault_core:: cipher:: {
19+ Card , Cipher , DecryptOptions , Identity , Login , LoginUri , PlainCard , PlainCipher , PlainIdentity ,
20+ } ;
1921use vault_ipc:: proto:: { Error as IpcError , Field , Item , ListEntry , Removed , Saved , Status } ;
2022
2123/// In-memory keys + ciphers held while the agent is unlocked.
@@ -258,6 +260,9 @@ pub struct CipherWrite {
258260 pub uri : Option < String > ,
259261 /// Card fields (card ciphers only); `number`/`code` are secret bytes.
260262 pub card : Option < vault_ipc:: proto:: CardWrite > ,
263+ /// Identity fields (identity ciphers only); ssn/passport/license are secret
264+ /// bytes.
265+ pub identity : Option < vault_ipc:: proto:: IdentityWrite > ,
261266}
262267
263268impl AgentState {
@@ -537,15 +542,21 @@ impl AgentState {
537542 . name
538543 . clone ( )
539544 . ok_or_else ( || IpcError :: Internal ( "add requires a name" . to_owned ( ) ) ) ?;
540- // Decode the card sub-object (type 3) before the struct literal moves
541- // the other `w` fields. `PlainCard::drop` scrubs number/code .
545+ // Decode the card/identity sub-objects before the struct literal moves
546+ // the other `w` fields. Their `Drop`s scrub the secret members .
542547 let card = if cipher_type == 3 {
543548 w. card . map ( card_write_to_plain) . transpose ( ) ?
544549 } else {
545550 None
546551 } ;
552+ let identity = if cipher_type == 4 {
553+ w. identity . map ( identity_write_to_plain) . transpose ( ) ?
554+ } else {
555+ None
556+ } ;
547557 // `plain` owns every plaintext value; `PlainCipher::drop` scrubs the
548- // secret fields (password/totp/notes/card) when it falls out of scope.
558+ // secret fields (password/totp/notes/card/identity) when it falls out of
559+ // scope.
549560 let plain = PlainCipher {
550561 id : String :: new ( ) ,
551562 cipher_type,
@@ -557,8 +568,7 @@ impl AgentState {
557568 totp : bytes_to_string ( w. totp ) ?,
558569 primary_uri : w. uri ,
559570 card,
560- // Identity write isn't built yet (read-only).
561- identity : None ,
571+ identity,
562572 } ;
563573 let mut cipher = Cipher :: from_plain ( & plain, & v. user_enc , & v. user_mac ) ;
564574 v. ensure_online ( ) . await ?;
@@ -603,6 +613,10 @@ impl AgentState {
603613 ) ,
604614 None => ( None , None , None , None , None , None ) ,
605615 } ;
616+ // Identity edit: decode into an owned `PlainIdentity` (its `Drop` scrubs
617+ // ssn/passport/license); the overlay borrows `&str` out of it.
618+ let identity_present = w. identity . is_some ( ) ;
619+ let identity_plain = w. identity . map ( identity_write_to_plain) . transpose ( ) ?;
606620 let v = self . vault . as_mut ( ) . ok_or ( IpcError :: Locked ) ?;
607621
608622 let mut cipher = v. ciphers [ idx] . clone ( ) ;
@@ -611,6 +625,11 @@ impl AgentState {
611625 "card fields can only be edited on a card item" . to_owned ( ) ,
612626 ) ) ;
613627 }
628+ if identity_present && cipher. cipher_type != 4 {
629+ return Err ( IpcError :: Internal (
630+ "identity fields can only be edited on an identity item" . to_owned ( ) ,
631+ ) ) ;
632+ }
614633 let card = card_present. then ( || CardEdit {
615634 cardholder : card_cardholder. as_deref ( ) ,
616635 brand : card_brand. as_deref ( ) ,
@@ -619,6 +638,26 @@ impl AgentState {
619638 exp_year : card_exp_year. as_deref ( ) ,
620639 code : card_code. as_ref ( ) . map ( |z| z. as_str ( ) ) ,
621640 } ) ;
641+ let identity = identity_plain. as_ref ( ) . map ( |p| IdentityEdit {
642+ title : p. title . as_deref ( ) ,
643+ first_name : p. first_name . as_deref ( ) ,
644+ middle_name : p. middle_name . as_deref ( ) ,
645+ last_name : p. last_name . as_deref ( ) ,
646+ username : p. username . as_deref ( ) ,
647+ company : p. company . as_deref ( ) ,
648+ ssn : p. ssn . as_deref ( ) ,
649+ passport_number : p. passport_number . as_deref ( ) ,
650+ license_number : p. license_number . as_deref ( ) ,
651+ email : p. email . as_deref ( ) ,
652+ phone : p. phone . as_deref ( ) ,
653+ address1 : p. address1 . as_deref ( ) ,
654+ address2 : p. address2 . as_deref ( ) ,
655+ address3 : p. address3 . as_deref ( ) ,
656+ city : p. city . as_deref ( ) ,
657+ state : p. state . as_deref ( ) ,
658+ postal_code : p. postal_code . as_deref ( ) ,
659+ country : p. country . as_deref ( ) ,
660+ } ) ;
622661 let overlay = EditOverlay {
623662 name : w. name . as_deref ( ) ,
624663 folder_id,
@@ -629,6 +668,7 @@ impl AgentState {
629668 totp : totp. as_ref ( ) . map ( |z| z. as_str ( ) ) ,
630669 uri : w. uri . as_deref ( ) ,
631670 card,
671+ identity,
632672 } ;
633673 apply_cipher_edits ( & mut cipher, & overlay, & v. user_enc , & v. user_mac ) ;
634674
@@ -943,6 +983,8 @@ struct EditOverlay<'a> {
943983 uri : Option < & ' a str > ,
944984 /// Card fields to set (card ciphers only); `Some` per field = change it.
945985 card : Option < CardEdit < ' a > > ,
986+ /// Identity fields to set (identity ciphers only); `Some` per field = change.
987+ identity : Option < IdentityEdit < ' a > > ,
946988}
947989
948990/// Card sub-overlay: borrowed plaintext for the card fields to change.
@@ -955,6 +997,28 @@ struct CardEdit<'a> {
955997 code : Option < & ' a str > ,
956998}
957999
1000+ /// Identity sub-overlay: borrowed plaintext for the identity fields to change.
1001+ struct IdentityEdit < ' a > {
1002+ title : Option < & ' a str > ,
1003+ first_name : Option < & ' a str > ,
1004+ middle_name : Option < & ' a str > ,
1005+ last_name : Option < & ' a str > ,
1006+ username : Option < & ' a str > ,
1007+ company : Option < & ' a str > ,
1008+ ssn : Option < & ' a str > ,
1009+ passport_number : Option < & ' a str > ,
1010+ license_number : Option < & ' a str > ,
1011+ email : Option < & ' a str > ,
1012+ phone : Option < & ' a str > ,
1013+ address1 : Option < & ' a str > ,
1014+ address2 : Option < & ' a str > ,
1015+ address3 : Option < & ' a str > ,
1016+ city : Option < & ' a str > ,
1017+ state : Option < & ' a str > ,
1018+ postal_code : Option < & ' a str > ,
1019+ country : Option < & ' a str > ,
1020+ }
1021+
9581022/// Apply `o` to an already-encrypted `cipher` in place, re-encrypting only the
9591023/// changed fields under fresh IVs. Everything not named in the overlay —
9601024/// secondary URIs, custom `fields`, `organization_id` — is preserved verbatim.
@@ -1020,6 +1084,58 @@ fn apply_cipher_edits(
10201084 card. code = Some ( enc ( v) ) ;
10211085 }
10221086 }
1087+ // Identity fields (the caller already checked the cipher is an identity).
1088+ if let Some ( i) = & o. identity {
1089+ let id = cipher. identity . get_or_insert_with ( Identity :: default) ;
1090+ let set = |slot : & mut Option < String > , v : Option < & str > | {
1091+ if let Some ( v) = v {
1092+ * slot = Some ( enc ( v) ) ;
1093+ }
1094+ } ;
1095+ set ( & mut id. title , i. title ) ;
1096+ set ( & mut id. first_name , i. first_name ) ;
1097+ set ( & mut id. middle_name , i. middle_name ) ;
1098+ set ( & mut id. last_name , i. last_name ) ;
1099+ set ( & mut id. username , i. username ) ;
1100+ set ( & mut id. company , i. company ) ;
1101+ set ( & mut id. ssn , i. ssn ) ;
1102+ set ( & mut id. passport_number , i. passport_number ) ;
1103+ set ( & mut id. license_number , i. license_number ) ;
1104+ set ( & mut id. email , i. email ) ;
1105+ set ( & mut id. phone , i. phone ) ;
1106+ set ( & mut id. address1 , i. address1 ) ;
1107+ set ( & mut id. address2 , i. address2 ) ;
1108+ set ( & mut id. address3 , i. address3 ) ;
1109+ set ( & mut id. city , i. city ) ;
1110+ set ( & mut id. state , i. state ) ;
1111+ set ( & mut id. postal_code , i. postal_code ) ;
1112+ set ( & mut id. country , i. country ) ;
1113+ }
1114+ }
1115+
1116+ /// Convert a wire `IdentityWrite` to a `PlainIdentity`, decoding the secret
1117+ /// ssn/passport/license bytes to strings (zeroized on failure / on drop).
1118+ fn identity_write_to_plain ( iw : vault_ipc:: proto:: IdentityWrite ) -> Result < PlainIdentity , IpcError > {
1119+ Ok ( PlainIdentity {
1120+ title : iw. title ,
1121+ first_name : iw. first_name ,
1122+ middle_name : iw. middle_name ,
1123+ last_name : iw. last_name ,
1124+ username : iw. username ,
1125+ company : iw. company ,
1126+ ssn : bytes_to_string ( iw. ssn ) ?,
1127+ passport_number : bytes_to_string ( iw. passport_number ) ?,
1128+ license_number : bytes_to_string ( iw. license_number ) ?,
1129+ email : iw. email ,
1130+ phone : iw. phone ,
1131+ address1 : iw. address1 ,
1132+ address2 : iw. address2 ,
1133+ address3 : iw. address3 ,
1134+ city : iw. city ,
1135+ state : iw. state ,
1136+ postal_code : iw. postal_code ,
1137+ country : iw. country ,
1138+ } )
10231139}
10241140
10251141/// Convert a wire `CardWrite` to a `PlainCard`, decoding the secret number/code
@@ -1456,6 +1572,7 @@ mod tests {
14561572 totp : None ,
14571573 uri : None ,
14581574 card : None ,
1575+ identity : None ,
14591576 } ;
14601577 apply_cipher_edits ( & mut cipher, & overlay, & enc, & mac) ;
14611578
@@ -1484,6 +1601,7 @@ mod tests {
14841601 totp : None ,
14851602 uri : Some ( "https://new.example" ) ,
14861603 card : None ,
1604+ identity : None ,
14871605 } ;
14881606 apply_cipher_edits ( & mut cipher, & overlay, & enc, & mac) ;
14891607
@@ -1527,6 +1645,7 @@ mod tests {
15271645 exp_year : Some ( "2031" ) ,
15281646 code : None ,
15291647 } ) ,
1648+ identity : None ,
15301649 } ;
15311650 apply_cipher_edits ( & mut cipher, & overlay, & enc, & mac) ;
15321651
@@ -1548,6 +1667,76 @@ mod tests {
15481667 assert_eq ! ( c. number. as_deref( ) , Some ( "4111111111111111" ) ) ;
15491668 }
15501669
1670+ #[ test]
1671+ fn apply_identity_edits_sets_only_given_fields ( ) {
1672+ let enc = [ 0x31u8 ; 32 ] ;
1673+ let mac = [ 0x32u8 ; 32 ] ;
1674+ let e =
1675+ |s : & str | Some ( vault_core:: EncString :: encrypt ( & enc, & mac, s. as_bytes ( ) ) . serialize ( ) ) ;
1676+ // An identity with an existing name + ssn; edit only the email + city.
1677+ let mut cipher = Cipher {
1678+ id : "id-1" . into ( ) ,
1679+ cipher_type : 4 ,
1680+ identity : Some ( Identity {
1681+ first_name : e ( "Jane" ) ,
1682+ last_name : e ( "Doe" ) ,
1683+ ssn : e ( "123-45-6789" ) ,
1684+ ..Identity :: default ( )
1685+ } ) ,
1686+ ..Cipher :: default ( )
1687+ } ;
1688+ let overlay = EditOverlay {
1689+ name : None ,
1690+ folder_id : None ,
1691+ folder_provided : false ,
1692+ notes : None ,
1693+ username : None ,
1694+ password : None ,
1695+ totp : None ,
1696+ uri : None ,
1697+ card : None ,
1698+ identity : Some ( IdentityEdit {
1699+ title : None ,
1700+ first_name : None ,
1701+ middle_name : None ,
1702+ last_name : None ,
1703+ username : None ,
1704+ company : None ,
1705+ ssn : None ,
1706+ passport_number : None ,
1707+ license_number : None ,
1708+ email : Some ( "jane@example.org" ) ,
1709+ phone : None ,
1710+ address1 : None ,
1711+ address2 : None ,
1712+ address3 : None ,
1713+ city : Some ( "Amber" ) ,
1714+ state : None ,
1715+ postal_code : None ,
1716+ country : None ,
1717+ } ) ,
1718+ } ;
1719+ apply_cipher_edits ( & mut cipher, & overlay, & enc, & mac) ;
1720+
1721+ let back = cipher
1722+ . decrypt (
1723+ & enc,
1724+ & mac,
1725+ DecryptOptions {
1726+ identity : true ,
1727+ ..DecryptOptions :: default ( )
1728+ } ,
1729+ )
1730+ . unwrap ( ) ;
1731+ let i = back. identity . as_ref ( ) . unwrap ( ) ;
1732+ assert_eq ! ( i. email. as_deref( ) , Some ( "jane@example.org" ) , "email set" ) ;
1733+ assert_eq ! ( i. city. as_deref( ) , Some ( "Amber" ) , "city set" ) ;
1734+ // Untouched fields preserved.
1735+ assert_eq ! ( i. first_name. as_deref( ) , Some ( "Jane" ) ) ;
1736+ assert_eq ! ( i. last_name. as_deref( ) , Some ( "Doe" ) ) ;
1737+ assert_eq ! ( i. ssn. as_deref( ) , Some ( "123-45-6789" ) ) ;
1738+ }
1739+
15511740 fn login_with_two_uris ( enc : & [ u8 ; 32 ] , mac : & [ u8 ; 32 ] ) -> Cipher {
15521741 let uri = |s : & str | LoginUri {
15531742 uri : Some ( vault_core:: EncString :: encrypt ( enc, mac, s. as_bytes ( ) ) . serialize ( ) ) ,
0 commit comments