@@ -74,17 +74,7 @@ impl Keystore {
7474 /// blob is the wrong length, or [`KeystoreError::Backend`] on a backend
7575 /// failure.
7676 pub fn load_master_key ( & self ) -> Result < MasterKey , KeystoreError > {
77- let secret = match self . entry . get_secret ( ) {
78- Ok ( s) => Zeroizing :: new ( s) ,
79- Err ( keyring:: Error :: NoEntry ) => return Err ( KeystoreError :: NotFound ) ,
80- Err ( e) => return Err ( KeystoreError :: Backend ( e) ) ,
81- } ;
82- if secret. len ( ) != KEY_LEN {
83- return Err ( KeystoreError :: MalformedKey ( secret. len ( ) ) ) ;
84- }
85- let mut bytes = [ 0u8 ; KEY_LEN ] ;
86- bytes. copy_from_slice ( & secret) ;
87- Ok ( MasterKey :: from_bytes ( bytes) )
77+ map_load_secret ( self . entry . get_secret ( ) )
8878 }
8979
9080 /// Deletes the master key entry (account removal / encryption opt-out).
@@ -94,9 +84,111 @@ impl Keystore {
9484 /// Returns [`KeystoreError::Backend`] on a backend failure other than a
9585 /// missing entry.
9686 pub fn delete_master_key ( & self ) -> Result < ( ) , KeystoreError > {
97- match self . entry . delete_credential ( ) {
98- Ok ( ( ) ) | Err ( keyring:: Error :: NoEntry ) => Ok ( ( ) ) ,
99- Err ( e) => Err ( KeystoreError :: Backend ( e) ) ,
100- }
87+ map_delete_result ( self . entry . delete_credential ( ) )
88+ }
89+ }
90+
91+ /// Maps a `keyring` `get_secret` result onto a loaded [`MasterKey`].
92+ ///
93+ /// This is the keyring-result -> domain mapping that is Driven's own
94+ /// responsibility (length validation, `NoEntry` -> recovery-phrase signal),
95+ /// split out as a PURE free fn so it is unit-tested WITHOUT an OS keychain -
96+ /// the same testability pattern as `driven-drive`'s
97+ /// `token_store::map_load_result` (the 4.1.2 mock store is not a declared
98+ /// dependency and a real round-trip would be flaky on headless CI). A missing
99+ /// entry maps to [`KeystoreError::NotFound`]; a secret that is not exactly
100+ /// [`KEY_LEN`] bytes maps to [`KeystoreError::MalformedKey`]; any other
101+ /// backend error maps to [`KeystoreError::Backend`]. The retrieved bytes are
102+ /// held in a [`Zeroizing`] buffer and scrubbed after the copy into the key.
103+ fn map_load_secret ( result : keyring:: Result < Vec < u8 > > ) -> Result < MasterKey , KeystoreError > {
104+ let secret = match result {
105+ Ok ( s) => Zeroizing :: new ( s) ,
106+ Err ( keyring:: Error :: NoEntry ) => return Err ( KeystoreError :: NotFound ) ,
107+ Err ( e) => return Err ( KeystoreError :: Backend ( e) ) ,
108+ } ;
109+ if secret. len ( ) != KEY_LEN {
110+ return Err ( KeystoreError :: MalformedKey ( secret. len ( ) ) ) ;
111+ }
112+ let mut bytes = [ 0u8 ; KEY_LEN ] ;
113+ bytes. copy_from_slice ( & secret) ;
114+ Ok ( MasterKey :: from_bytes ( bytes) )
115+ }
116+
117+ /// Maps a `keyring` `delete_credential` result onto the idempotent-delete
118+ /// domain result (pure, OS-keychain-free; mirrors `driven-drive`'s
119+ /// `token_store::map_delete_result`). A missing entry is NOT an error
120+ /// (delete is idempotent); any other backend failure maps to
121+ /// [`KeystoreError::Backend`].
122+ fn map_delete_result ( result : keyring:: Result < ( ) > ) -> Result < ( ) , KeystoreError > {
123+ match result {
124+ Ok ( ( ) ) | Err ( keyring:: Error :: NoEntry ) => Ok ( ( ) ) ,
125+ Err ( e) => Err ( KeystoreError :: Backend ( e) ) ,
126+ }
127+ }
128+
129+ #[ cfg( test) ]
130+ mod tests {
131+ use super :: * ;
132+
133+ #[ test]
134+ fn load_maps_no_entry_to_not_found ( ) {
135+ // A missing keychain entry is the first-run / wiped-keychain signal the
136+ // recovery-phrase flow keys off - it must NOT be a generic backend error.
137+ assert ! ( matches!(
138+ map_load_secret( Err ( keyring:: Error :: NoEntry ) ) ,
139+ Err ( KeystoreError :: NotFound )
140+ ) ) ;
141+ }
142+
143+ #[ test]
144+ fn load_maps_correct_length_secret_to_master_key ( ) {
145+ // A well-formed 32-byte secret reconstructs the master key byte-for-byte.
146+ let raw = [ 7u8 ; KEY_LEN ] ;
147+ let key = map_load_secret ( Ok ( raw. to_vec ( ) ) ) . unwrap ( ) ;
148+ assert_eq ! ( key. as_bytes( ) , & raw) ;
149+ }
150+
151+ #[ test]
152+ fn load_rejects_wrong_length_secret_as_malformed ( ) {
153+ // A foreign / corrupt write of the wrong length must surface MalformedKey
154+ // carrying the observed length - never be silently truncated or panic.
155+ assert ! ( matches!(
156+ map_load_secret( Ok ( vec![ 0u8 ; 16 ] ) ) ,
157+ Err ( KeystoreError :: MalformedKey ( 16 ) )
158+ ) ) ;
159+ assert ! ( matches!(
160+ map_load_secret( Ok ( Vec :: new( ) ) ) ,
161+ Err ( KeystoreError :: MalformedKey ( 0 ) )
162+ ) ) ;
163+ assert ! ( matches!(
164+ map_load_secret( Ok ( vec![ 0u8 ; KEY_LEN + 1 ] ) ) ,
165+ Err ( KeystoreError :: MalformedKey ( n) ) if n == KEY_LEN + 1
166+ ) ) ;
167+ }
168+
169+ #[ test]
170+ fn load_maps_other_backend_error ( ) {
171+ // A real backend failure (anything but NoEntry) is preserved as Backend.
172+ let r = map_load_secret ( Err ( keyring:: Error :: Invalid (
173+ "service" . to_string ( ) ,
174+ "boom" . to_string ( ) ,
175+ ) ) ) ;
176+ assert ! ( matches!( r, Err ( KeystoreError :: Backend ( _) ) ) ) ;
177+ }
178+
179+ #[ test]
180+ fn delete_is_idempotent_for_missing_entry ( ) {
181+ // Both a successful delete and a no-such-entry delete are Ok (idempotent).
182+ assert ! ( map_delete_result( Ok ( ( ) ) ) . is_ok( ) ) ;
183+ assert ! ( map_delete_result( Err ( keyring:: Error :: NoEntry ) ) . is_ok( ) ) ;
184+ }
185+
186+ #[ test]
187+ fn delete_surfaces_other_backend_error ( ) {
188+ let r = map_delete_result ( Err ( keyring:: Error :: Invalid (
189+ "service" . to_string ( ) ,
190+ "boom" . to_string ( ) ,
191+ ) ) ) ;
192+ assert ! ( matches!( r, Err ( KeystoreError :: Backend ( _) ) ) ) ;
101193 }
102194}
0 commit comments