@@ -12,6 +12,10 @@ use std::collections::HashMap;
1212use std:: path:: PathBuf ;
1313use std:: fs;
1414use std:: sync:: Arc ;
15+ use data_encoding:: { BASE32 , BASE32_NOPAD } ;
16+ use hmac:: { Hmac , Mac } ;
17+ use sha1:: Sha1 ;
18+ use sha2:: { Sha256 , Sha512 } ;
1519
1620/// Initialize the Persona service with master password
1721#[ command]
@@ -196,6 +200,85 @@ pub async fn get_identity(
196200 }
197201}
198202
203+ /// Update an existing identity
204+ #[ command]
205+ pub async fn update_identity (
206+ request : UpdateIdentityRequest ,
207+ state : State < ' _ , AppState > ,
208+ ) -> std:: result:: Result < ApiResponse < SerializableIdentity > , String > {
209+ let service_guard = state. service . lock ( ) . await ;
210+ match service_guard. as_ref ( ) {
211+ Some ( service) => {
212+ let uuid = match Uuid :: from_str ( & request. id ) {
213+ Ok ( uuid) => uuid,
214+ Err ( _) => return Ok ( ApiResponse :: error ( "Invalid UUID format" . to_string ( ) ) ) ,
215+ } ;
216+
217+ match service. get_identity ( & uuid) . await {
218+ Ok ( Some ( mut identity) ) => {
219+ let identity_type = match request. identity_type . as_str ( ) {
220+ "Personal" => IdentityType :: Personal ,
221+ "Work" => IdentityType :: Work ,
222+ "Social" => IdentityType :: Social ,
223+ "Financial" => IdentityType :: Financial ,
224+ "Gaming" => IdentityType :: Gaming ,
225+ custom => IdentityType :: Custom ( custom. to_string ( ) ) ,
226+ } ;
227+
228+ identity. name = request. name . trim ( ) . to_string ( ) ;
229+ identity. identity_type = identity_type;
230+ identity. description = request. description . and_then ( |s| {
231+ let trimmed = s. trim ( ) . to_string ( ) ;
232+ if trimmed. is_empty ( ) { None } else { Some ( trimmed) }
233+ } ) ;
234+ identity. email = request. email . and_then ( |s| {
235+ let trimmed = s. trim ( ) . to_string ( ) ;
236+ if trimmed. is_empty ( ) { None } else { Some ( trimmed) }
237+ } ) ;
238+ identity. phone = request. phone . and_then ( |s| {
239+ let trimmed = s. trim ( ) . to_string ( ) ;
240+ if trimmed. is_empty ( ) { None } else { Some ( trimmed) }
241+ } ) ;
242+ if let Some ( tags) = request. tags {
243+ identity. tags = tags
244+ . into_iter ( )
245+ . map ( |t| t. trim ( ) . to_string ( ) )
246+ . filter ( |t| !t. is_empty ( ) )
247+ . collect ( ) ;
248+ }
249+
250+ match service. update_identity ( & identity) . await {
251+ Ok ( updated_identity) => Ok ( ApiResponse :: success ( updated_identity. into ( ) ) ) ,
252+ Err ( e) => Ok ( ApiResponse :: error ( format ! ( "Failed to update identity: {}" , e) ) ) ,
253+ }
254+ }
255+ Ok ( None ) => Ok ( ApiResponse :: error ( "Identity not found" . to_string ( ) ) ) ,
256+ Err ( e) => Ok ( ApiResponse :: error ( format ! ( "Failed to get identity: {}" , e) ) ) ,
257+ }
258+ }
259+ None => Ok ( ApiResponse :: error ( "Service not initialized" . to_string ( ) ) ) ,
260+ }
261+ }
262+
263+ /// Delete an identity
264+ #[ command]
265+ pub async fn delete_identity (
266+ identity_id : String ,
267+ state : State < ' _ , AppState > ,
268+ ) -> std:: result:: Result < ApiResponse < bool > , String > {
269+ let service_guard = state. service . lock ( ) . await ;
270+ match service_guard. as_ref ( ) {
271+ Some ( service) => match Uuid :: from_str ( & identity_id) {
272+ Ok ( uuid) => match service. delete_identity ( & uuid) . await {
273+ Ok ( ok) => Ok ( ApiResponse :: success ( ok) ) ,
274+ Err ( e) => Ok ( ApiResponse :: error ( format ! ( "Failed to delete identity: {}" , e) ) ) ,
275+ } ,
276+ Err ( _) => Ok ( ApiResponse :: error ( "Invalid UUID format" . to_string ( ) ) ) ,
277+ } ,
278+ None => Ok ( ApiResponse :: error ( "Service not initialized" . to_string ( ) ) ) ,
279+ }
280+ }
281+
199282/// Create a new credential
200283#[ command]
201284pub async fn create_credential (
@@ -244,6 +327,17 @@ pub async fn create_credential(
244327 if let Some ( username) = request. username {
245328 credential. username = Some ( username) ;
246329 }
330+ if let Some ( notes) = request. notes {
331+ let trimmed = notes. trim ( ) . to_string ( ) ;
332+ credential. notes = if trimmed. is_empty ( ) { None } else { Some ( trimmed) } ;
333+ }
334+ if let Some ( tags) = request. tags {
335+ credential. tags = tags
336+ . into_iter ( )
337+ . map ( |t| t. trim ( ) . to_string ( ) )
338+ . filter ( |t| !t. is_empty ( ) )
339+ . collect ( ) ;
340+ }
247341
248342 match service. update_credential ( & credential) . await {
249343 Ok ( updated_credential) => Ok ( ApiResponse :: success ( updated_credential. into ( ) ) ) ,
@@ -305,8 +399,10 @@ pub async fn get_credential_data(
305399 CredentialData :: CryptoWallet ( _) => "CryptoWallet" . to_string ( ) ,
306400 CredentialData :: SshKey ( _) => "SshKey" . to_string ( ) ,
307401 CredentialData :: ApiKey ( _) => "ApiKey" . to_string ( ) ,
402+ CredentialData :: BankCard ( _) => "BankCard" . to_string ( ) ,
403+ CredentialData :: ServerConfig ( _) => "ServerConfig" . to_string ( ) ,
404+ CredentialData :: TwoFactor ( _) => "TwoFactor" . to_string ( ) ,
308405 CredentialData :: Raw ( _) => "Raw" . to_string ( ) ,
309- _ => "Unknown" . to_string ( ) ,
310406 } ,
311407 data : credential_data_to_json ( & data) ,
312408 } ) ;
@@ -322,6 +418,52 @@ pub async fn get_credential_data(
322418 }
323419}
324420
421+ /// Generate a TOTP code for a TwoFactor credential (without exposing the secret)
422+ #[ command]
423+ pub async fn get_totp_code (
424+ credential_id : String ,
425+ state : State < ' _ , AppState > ,
426+ ) -> std:: result:: Result < ApiResponse < TotpCodeResponse > , String > {
427+ let service_guard = state. service . lock ( ) . await ;
428+ let service = service_guard
429+ . as_ref ( )
430+ . ok_or_else ( || "Service not initialized" . to_string ( ) ) ?;
431+
432+ let uuid = Uuid :: from_str ( & credential_id) . map_err ( |_| "Invalid UUID format" . to_string ( ) ) ?;
433+ let credential_data = service
434+ . get_credential_data ( & uuid)
435+ . await
436+ . map_err ( |e| format ! ( "Failed to get credential data: {}" , e) ) ?;
437+
438+ let data = credential_data. ok_or_else ( || "Credential not found" . to_string ( ) ) ?;
439+ match data {
440+ CredentialData :: TwoFactor ( tf) => {
441+ let secret_bytes = decode_totp_secret ( & tf. secret_key ) ?;
442+ let now = chrono:: Utc :: now ( ) ;
443+ let period = tf. period . max ( 1 ) as u64 ;
444+ let timestamp = now. timestamp ( ) . max ( 0 ) as u64 ;
445+ let counter = timestamp / period;
446+ let digits = tf. digits . clamp ( 4 , 10 ) as u32 ;
447+ let code_num = hotp ( & secret_bytes, counter, & tf. algorithm ) ?;
448+ let modulo = 10_u32 . pow ( digits) ;
449+ let value = code_num % modulo;
450+ let code = format ! ( "{:0width$}" , value, width = digits as usize ) ;
451+ let remaining = ( period - ( timestamp % period) ) as u32 ;
452+
453+ Ok ( ApiResponse :: success ( TotpCodeResponse {
454+ code,
455+ remaining_seconds : remaining,
456+ period : tf. period . max ( 1 ) ,
457+ digits : tf. digits . clamp ( 4 , 10 ) ,
458+ algorithm : tf. algorithm ,
459+ issuer : tf. issuer ,
460+ account_name : tf. account_name ,
461+ } ) )
462+ }
463+ _ => Ok ( ApiResponse :: error ( "Credential is not a TwoFactor entry" . to_string ( ) ) ) ,
464+ }
465+ }
466+
325467/// Search credentials
326468#[ command]
327469pub async fn search_credentials (
@@ -343,6 +485,54 @@ pub async fn search_credentials(
343485 }
344486}
345487
488+ fn hotp ( secret : & [ u8 ] , counter : u64 , algorithm : & str ) -> std:: result:: Result < u32 , String > {
489+ let msg = counter. to_be_bytes ( ) ;
490+ let algo = algorithm. to_ascii_uppercase ( ) ;
491+
492+ let hash = if algo == "SHA256" {
493+ type HmacSha256 = Hmac < Sha256 > ;
494+ let mut mac = HmacSha256 :: new_from_slice ( secret) . map_err ( |_| "Invalid secret" . to_string ( ) ) ?;
495+ mac. update ( & msg) ;
496+ mac. finalize ( ) . into_bytes ( ) . to_vec ( )
497+ } else if algo == "SHA512" {
498+ type HmacSha512 = Hmac < Sha512 > ;
499+ let mut mac = HmacSha512 :: new_from_slice ( secret) . map_err ( |_| "Invalid secret" . to_string ( ) ) ?;
500+ mac. update ( & msg) ;
501+ mac. finalize ( ) . into_bytes ( ) . to_vec ( )
502+ } else {
503+ type HmacSha1 = Hmac < Sha1 > ;
504+ let mut mac = HmacSha1 :: new_from_slice ( secret) . map_err ( |_| "Invalid secret" . to_string ( ) ) ?;
505+ mac. update ( & msg) ;
506+ mac. finalize ( ) . into_bytes ( ) . to_vec ( )
507+ } ;
508+
509+ let offset = ( hash. last ( ) . copied ( ) . unwrap_or ( 0 ) & 0x0f ) as usize ;
510+ if offset + 4 > hash. len ( ) {
511+ return Err ( "Invalid HMAC output" . to_string ( ) ) ;
512+ }
513+ let slice = & hash[ offset..offset + 4 ] ;
514+ let binary = ( ( slice[ 0 ] as u32 & 0x7f ) << 24 )
515+ | ( ( slice[ 1 ] as u32 ) << 16 )
516+ | ( ( slice[ 2 ] as u32 ) << 8 )
517+ | slice[ 3 ] as u32 ;
518+ Ok ( binary)
519+ }
520+
521+ fn decode_totp_secret ( secret : & str ) -> std:: result:: Result < Vec < u8 > , String > {
522+ let normalized: String = secret
523+ . chars ( )
524+ . filter ( |c| !c. is_whitespace ( ) )
525+ . map ( |c| c. to_ascii_uppercase ( ) )
526+ . collect :: < String > ( )
527+ . trim_matches ( '=' )
528+ . to_string ( ) ;
529+
530+ BASE32_NOPAD
531+ . decode ( normalized. as_bytes ( ) )
532+ . or_else ( |_| BASE32 . decode ( normalized. as_bytes ( ) ) )
533+ . map_err ( |e| format ! ( "Invalid base32 secret: {}" , e) )
534+ }
535+
346536/// Generate password
347537#[ command]
348538pub async fn generate_password (
0 commit comments