@@ -15,7 +15,7 @@ use zeroize::Zeroizing;
1515
1616use vault_api:: BitwardenClient ;
1717use vault_core:: EncString ;
18- use vault_core:: cipher:: { Cipher , DecryptOptions , Login , LoginUri , PlainCipher } ;
18+ use vault_core:: cipher:: { Card , Cipher , DecryptOptions , Login , LoginUri , PlainCard , PlainCipher } ;
1919use vault_ipc:: proto:: { Error as IpcError , Field , Item , ListEntry , Removed , Saved , Status } ;
2020
2121/// In-memory keys + ciphers held while the agent is unlocked.
@@ -256,6 +256,8 @@ pub struct CipherWrite {
256256 pub totp : Option < Vec < u8 > > ,
257257 /// Primary login URI.
258258 pub uri : Option < String > ,
259+ /// Card fields (card ciphers only); `number`/`code` are secret bytes.
260+ pub card : Option < vault_ipc:: proto:: CardWrite > ,
259261}
260262
261263impl AgentState {
@@ -535,8 +537,15 @@ impl AgentState {
535537 . name
536538 . clone ( )
537539 . 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.
542+ let card = if cipher_type == 3 {
543+ w. card . map ( card_write_to_plain) . transpose ( ) ?
544+ } else {
545+ None
546+ } ;
538547 // `plain` owns every plaintext value; `PlainCipher::drop` scrubs the
539- // secret fields (password/totp/notes) when it falls out of scope.
548+ // secret fields (password/totp/notes/card ) when it falls out of scope.
540549 let plain = PlainCipher {
541550 id : String :: new ( ) ,
542551 cipher_type,
@@ -547,8 +556,8 @@ impl AgentState {
547556 password : bytes_to_string ( w. password ) ?,
548557 totp : bytes_to_string ( w. totp ) ?,
549558 primary_uri : w. uri ,
550- // add/edit build logins only; card/identity aren't created here.
551- card : None ,
559+ card,
560+ // Identity write isn't built yet (read-only).
552561 identity : None ,
553562 } ;
554563 let mut cipher = Cipher :: from_plain ( & plain, & v. user_enc , & v. user_mac ) ;
@@ -579,9 +588,37 @@ impl AgentState {
579588 // the plaintext when the locals drop at the end of this call.
580589 let password = bytes_to_string ( w. password ) ?. map ( Zeroizing :: new) ;
581590 let totp = bytes_to_string ( w. totp ) ?. map ( Zeroizing :: new) ;
591+ // Card edit: decode its secret bytes; non-secret fields stay owned for
592+ // borrowing into the overlay.
593+ let card_present = w. card . is_some ( ) ;
594+ let ( card_cardholder, card_brand, card_exp_month, card_exp_year, card_number, card_code) =
595+ match w. card {
596+ Some ( cw) => (
597+ cw. cardholder ,
598+ cw. brand ,
599+ cw. exp_month ,
600+ cw. exp_year ,
601+ bytes_to_string ( cw. number ) ?. map ( Zeroizing :: new) ,
602+ bytes_to_string ( cw. code ) ?. map ( Zeroizing :: new) ,
603+ ) ,
604+ None => ( None , None , None , None , None , None ) ,
605+ } ;
582606 let v = self . vault . as_mut ( ) . ok_or ( IpcError :: Locked ) ?;
583607
584608 let mut cipher = v. ciphers [ idx] . clone ( ) ;
609+ if card_present && cipher. cipher_type != 3 {
610+ return Err ( IpcError :: Internal (
611+ "card fields can only be edited on a card item" . to_owned ( ) ,
612+ ) ) ;
613+ }
614+ let card = card_present. then ( || CardEdit {
615+ cardholder : card_cardholder. as_deref ( ) ,
616+ brand : card_brand. as_deref ( ) ,
617+ number : card_number. as_ref ( ) . map ( |z| z. as_str ( ) ) ,
618+ exp_month : card_exp_month. as_deref ( ) ,
619+ exp_year : card_exp_year. as_deref ( ) ,
620+ code : card_code. as_ref ( ) . map ( |z| z. as_str ( ) ) ,
621+ } ) ;
585622 let overlay = EditOverlay {
586623 name : w. name . as_deref ( ) ,
587624 folder_id,
@@ -591,6 +628,7 @@ impl AgentState {
591628 password : password. as_ref ( ) . map ( |z| z. as_str ( ) ) ,
592629 totp : totp. as_ref ( ) . map ( |z| z. as_str ( ) ) ,
593630 uri : w. uri . as_deref ( ) ,
631+ card,
594632 } ;
595633 apply_cipher_edits ( & mut cipher, & overlay, & v. user_enc , & v. user_mac ) ;
596634
@@ -903,6 +941,18 @@ struct EditOverlay<'a> {
903941 password : Option < & ' a str > ,
904942 totp : Option < & ' a str > ,
905943 uri : Option < & ' a str > ,
944+ /// Card fields to set (card ciphers only); `Some` per field = change it.
945+ card : Option < CardEdit < ' a > > ,
946+ }
947+
948+ /// Card sub-overlay: borrowed plaintext for the card fields to change.
949+ struct CardEdit < ' a > {
950+ cardholder : Option < & ' a str > ,
951+ brand : Option < & ' a str > ,
952+ number : Option < & ' a str > ,
953+ exp_month : Option < & ' a str > ,
954+ exp_year : Option < & ' a str > ,
955+ code : Option < & ' a str > ,
906956}
907957
908958/// Apply `o` to an already-encrypted `cipher` in place, re-encrypting only the
@@ -948,6 +998,41 @@ fn apply_cipher_edits(
948998 login. uris = Some ( uris) ;
949999 }
9501000 }
1001+ // Card fields (the caller already checked the cipher is a card).
1002+ if let Some ( c) = & o. card {
1003+ let card = cipher. card . get_or_insert_with ( Card :: default) ;
1004+ if let Some ( v) = c. cardholder {
1005+ card. cardholder_name = Some ( enc ( v) ) ;
1006+ }
1007+ if let Some ( v) = c. brand {
1008+ card. brand = Some ( enc ( v) ) ;
1009+ }
1010+ if let Some ( v) = c. number {
1011+ card. number = Some ( enc ( v) ) ;
1012+ }
1013+ if let Some ( v) = c. exp_month {
1014+ card. exp_month = Some ( enc ( v) ) ;
1015+ }
1016+ if let Some ( v) = c. exp_year {
1017+ card. exp_year = Some ( enc ( v) ) ;
1018+ }
1019+ if let Some ( v) = c. code {
1020+ card. code = Some ( enc ( v) ) ;
1021+ }
1022+ }
1023+ }
1024+
1025+ /// Convert a wire `CardWrite` to a `PlainCard`, decoding the secret number/code
1026+ /// bytes to strings (zeroized on failure by `bytes_to_string`).
1027+ fn card_write_to_plain ( cw : vault_ipc:: proto:: CardWrite ) -> Result < PlainCard , IpcError > {
1028+ Ok ( PlainCard {
1029+ cardholder_name : cw. cardholder ,
1030+ brand : cw. brand ,
1031+ number : bytes_to_string ( cw. number ) ?,
1032+ exp_month : cw. exp_month ,
1033+ exp_year : cw. exp_year ,
1034+ code : bytes_to_string ( cw. code ) ?,
1035+ } )
9511036}
9521037
9531038#[ cfg( test) ]
@@ -1370,6 +1455,7 @@ mod tests {
13701455 password : None ,
13711456 totp : None ,
13721457 uri : None ,
1458+ card : None ,
13731459 } ;
13741460 apply_cipher_edits ( & mut cipher, & overlay, & enc, & mac) ;
13751461
@@ -1397,6 +1483,7 @@ mod tests {
13971483 password : None ,
13981484 totp : None ,
13991485 uri : Some ( "https://new.example" ) ,
1486+ card : None ,
14001487 } ;
14011488 apply_cipher_edits ( & mut cipher, & overlay, & enc, & mac) ;
14021489
@@ -1406,6 +1493,61 @@ mod tests {
14061493 assert_eq ! ( decrypt_uri( & uris[ 1 ] , & enc, & mac) , "https://two.example" ) ;
14071494 }
14081495
1496+ #[ test]
1497+ fn apply_card_edits_sets_only_given_fields ( ) {
1498+ let enc = [ 0x21u8 ; 32 ] ;
1499+ let mac = [ 0x22u8 ; 32 ] ;
1500+ let e =
1501+ |s : & str | Some ( vault_core:: EncString :: encrypt ( & enc, & mac, s. as_bytes ( ) ) . serialize ( ) ) ;
1502+ // A card with an existing brand + number; edit only the expiry.
1503+ let mut cipher = Cipher {
1504+ id : "card-1" . into ( ) ,
1505+ cipher_type : 3 ,
1506+ card : Some ( Card {
1507+ brand : e ( "Visa" ) ,
1508+ number : e ( "4111111111111111" ) ,
1509+ ..Card :: default ( )
1510+ } ) ,
1511+ ..Cipher :: default ( )
1512+ } ;
1513+ let overlay = EditOverlay {
1514+ name : None ,
1515+ folder_id : None ,
1516+ folder_provided : false ,
1517+ notes : None ,
1518+ username : None ,
1519+ password : None ,
1520+ totp : None ,
1521+ uri : None ,
1522+ card : Some ( CardEdit {
1523+ cardholder : None ,
1524+ brand : None ,
1525+ number : None ,
1526+ exp_month : Some ( "5" ) ,
1527+ exp_year : Some ( "2031" ) ,
1528+ code : None ,
1529+ } ) ,
1530+ } ;
1531+ apply_cipher_edits ( & mut cipher, & overlay, & enc, & mac) ;
1532+
1533+ let back = cipher
1534+ . decrypt (
1535+ & enc,
1536+ & mac,
1537+ DecryptOptions {
1538+ card : true ,
1539+ ..DecryptOptions :: default ( )
1540+ } ,
1541+ )
1542+ . unwrap ( ) ;
1543+ let c = back. card . as_ref ( ) . unwrap ( ) ;
1544+ assert_eq ! ( c. exp_month. as_deref( ) , Some ( "5" ) , "expiry updated" ) ;
1545+ assert_eq ! ( c. exp_year. as_deref( ) , Some ( "2031" ) ) ;
1546+ // Untouched fields preserved.
1547+ assert_eq ! ( c. brand. as_deref( ) , Some ( "Visa" ) ) ;
1548+ assert_eq ! ( c. number. as_deref( ) , Some ( "4111111111111111" ) ) ;
1549+ }
1550+
14091551 fn login_with_two_uris ( enc : & [ u8 ; 32 ] , mac : & [ u8 ; 32 ] ) -> Cipher {
14101552 let uri = |s : & str | LoginUri {
14111553 uri : Some ( vault_core:: EncString :: encrypt ( enc, mac, s. as_bytes ( ) ) . serialize ( ) ) ,
0 commit comments