@@ -401,13 +401,233 @@ const KEYRING_KEY_ACCESS: &str = "access_token";
401401const KEYRING_KEY_REFRESH : & str = "refresh_token" ;
402402const KEYRING_KEY_METADATA : & str = "metadata" ;
403403
404+ /// Windows Credential Manager has a BLOB size limit of 2560 bytes.
405+ /// We use a smaller chunk size to be safe and account for encoding overhead.
406+ /// See: https://learn.microsoft.com/en-us/windows/win32/api/wincred/ns-wincred-credentiala
407+ #[ cfg( target_os = "windows" ) ]
408+ const WINDOWS_CREDENTIAL_CHUNK_SIZE : usize = 2400 ;
409+
410+ /// Maximum number of chunks we support (allows up to ~24KB tokens which is more than enough)
411+ #[ cfg( target_os = "windows" ) ]
412+ const MAX_CREDENTIAL_CHUNKS : usize = 10 ;
413+
404414fn get_keyring_entry ( account : & str ) -> Result < keyring:: Entry > {
405415 keyring:: Entry :: new ( KEYRING_SERVICE , account) . context ( "Failed to access keyring" )
406416}
407417
418+ /// Windows-specific: Save a potentially large credential by splitting into chunks.
419+ ///
420+ /// Windows Credential Manager has a hard limit of 2560 bytes per credential BLOB.
421+ /// JWT tokens from OAuth can exceed this limit. This function splits large
422+ /// credentials into multiple keyring entries and stores the chunk count in
423+ /// a separate entry for reconstruction.
424+ #[ cfg( target_os = "windows" ) ]
425+ fn save_credential_chunked ( base_key : & str , value : & str ) -> Result < ( ) > {
426+ let value_bytes = value. as_bytes ( ) ;
427+ let total_len = value_bytes. len ( ) ;
428+
429+ // If it fits in a single chunk, store directly
430+ if total_len <= WINDOWS_CREDENTIAL_CHUNK_SIZE {
431+ tracing:: debug!(
432+ key = base_key,
433+ size = total_len,
434+ "Storing credential directly (within size limit)"
435+ ) ;
436+ get_keyring_entry ( base_key) ?
437+ . set_password ( value)
438+ . with_context ( || format ! ( "Failed to save {} to keyring" , base_key) ) ?;
439+
440+ // Clean up any old chunk entries that might exist
441+ delete_credential_chunks ( base_key) ?;
442+ return Ok ( ( ) ) ;
443+ }
444+
445+ // Calculate number of chunks needed
446+ let num_chunks =
447+ ( total_len + WINDOWS_CREDENTIAL_CHUNK_SIZE - 1 ) / WINDOWS_CREDENTIAL_CHUNK_SIZE ;
448+
449+ if num_chunks > MAX_CREDENTIAL_CHUNKS {
450+ return Err ( anyhow:: anyhow!(
451+ "Credential too large: {} bytes requires {} chunks, max is {}" ,
452+ total_len,
453+ num_chunks,
454+ MAX_CREDENTIAL_CHUNKS
455+ ) ) ;
456+ }
457+
458+ tracing:: debug!(
459+ key = base_key,
460+ size = total_len,
461+ chunks = num_chunks,
462+ "Splitting large credential into chunks for Windows Credential Manager"
463+ ) ;
464+
465+ // Store each chunk
466+ for ( i, chunk) in value_bytes
467+ . chunks ( WINDOWS_CREDENTIAL_CHUNK_SIZE )
468+ . enumerate ( )
469+ {
470+ let chunk_key = format ! ( "{}_chunk_{}" , base_key, i) ;
471+ let chunk_str = String :: from_utf8_lossy ( chunk) ;
472+
473+ get_keyring_entry ( & chunk_key) ?
474+ . set_password ( & chunk_str)
475+ . with_context ( || format ! ( "Failed to save chunk {} of {} to keyring" , i, base_key) ) ?;
476+ }
477+
478+ // Store chunk metadata (count) in the base key
479+ let chunk_info = format ! ( "__chunked__:{}" , num_chunks) ;
480+ get_keyring_entry ( base_key) ?
481+ . set_password ( & chunk_info)
482+ . with_context ( || format ! ( "Failed to save chunk metadata for {} to keyring" , base_key) ) ?;
483+
484+ // Clean up any extra old chunks that might exist from previous saves
485+ for i in num_chunks..MAX_CREDENTIAL_CHUNKS {
486+ let chunk_key = format ! ( "{}_chunk_{}" , base_key, i) ;
487+ if let Ok ( entry) = get_keyring_entry ( & chunk_key) {
488+ let _ = entry. delete_credential ( ) ;
489+ }
490+ }
491+
492+ Ok ( ( ) )
493+ }
494+
495+ /// Windows-specific: Load a potentially chunked credential.
496+ #[ cfg( target_os = "windows" ) ]
497+ fn load_credential_chunked ( base_key : & str ) -> Result < Option < String > > {
498+ let entry = get_keyring_entry ( base_key) ?;
499+
500+ match entry. get_password ( ) {
501+ Ok ( value) => {
502+ // Check if this is chunk metadata
503+ if let Some ( chunk_count_str) = value. strip_prefix ( "__chunked__:" ) {
504+ let num_chunks: usize = chunk_count_str
505+ . parse ( )
506+ . with_context ( || format ! ( "Invalid chunk count for {}" , base_key) ) ?;
507+
508+ tracing:: debug!(
509+ key = base_key,
510+ chunks = num_chunks,
511+ "Loading chunked credential from Windows Credential Manager"
512+ ) ;
513+
514+ // Reconstruct from chunks
515+ let mut full_value = String :: new ( ) ;
516+ for i in 0 ..num_chunks {
517+ let chunk_key = format ! ( "{}_chunk_{}" , base_key, i) ;
518+ let chunk_entry = get_keyring_entry ( & chunk_key) ?;
519+ let chunk = chunk_entry
520+ . get_password ( )
521+ . with_context ( || format ! ( "Failed to load chunk {} of {}" , i, base_key) ) ?;
522+ full_value. push_str ( & chunk) ;
523+ }
524+
525+ Ok ( Some ( full_value) )
526+ } else {
527+ // Regular non-chunked value
528+ Ok ( Some ( value) )
529+ }
530+ }
531+ Err ( keyring:: Error :: NoEntry ) => Ok ( None ) ,
532+ Err ( e) => Err ( anyhow:: anyhow!( "Keyring error loading {}: {}" , base_key, e) ) ,
533+ }
534+ }
535+
536+ /// Windows-specific: Delete a credential and all its chunks.
537+ #[ cfg( target_os = "windows" ) ]
538+ fn delete_credential_chunked ( base_key : & str ) -> Result < bool > {
539+ let mut deleted = false ;
540+
541+ // Delete all possible chunks
542+ deleted |= delete_credential_chunks ( base_key) ?;
543+
544+ // Delete the base entry
545+ if let Ok ( entry) = get_keyring_entry ( base_key) {
546+ match entry. delete_credential ( ) {
547+ Ok ( ( ) ) => deleted = true ,
548+ Err ( keyring:: Error :: NoEntry ) => { }
549+ Err ( e) => {
550+ return Err ( anyhow:: anyhow!(
551+ "Keyring error deleting {}: {}" ,
552+ base_key,
553+ e
554+ ) ) ;
555+ }
556+ }
557+ }
558+
559+ Ok ( deleted)
560+ }
561+
562+ /// Windows-specific: Delete all chunk entries for a credential.
563+ #[ cfg( target_os = "windows" ) ]
564+ fn delete_credential_chunks ( base_key : & str ) -> Result < bool > {
565+ let mut deleted = false ;
566+
567+ for i in 0 ..MAX_CREDENTIAL_CHUNKS {
568+ let chunk_key = format ! ( "{}_chunk_{}" , base_key, i) ;
569+ if let Ok ( entry) = get_keyring_entry ( & chunk_key) {
570+ match entry. delete_credential ( ) {
571+ Ok ( ( ) ) => deleted = true ,
572+ Err ( keyring:: Error :: NoEntry ) => { }
573+ Err ( e) => {
574+ tracing:: debug!(
575+ key = chunk_key,
576+ error = %e,
577+ "Failed to delete chunk (may not exist)"
578+ ) ;
579+ }
580+ }
581+ }
582+ }
583+
584+ Ok ( deleted)
585+ }
586+
587+ /// Non-Windows: Save credential directly (no size limit issues).
588+ #[ cfg( not( target_os = "windows" ) ) ]
589+ fn save_credential_chunked ( base_key : & str , value : & str ) -> Result < ( ) > {
590+ get_keyring_entry ( base_key) ?
591+ . set_password ( value)
592+ . with_context ( || format ! ( "Failed to save {} to keyring" , base_key) )
593+ }
594+
595+ /// Non-Windows: Load credential directly.
596+ #[ cfg( not( target_os = "windows" ) ) ]
597+ fn load_credential_chunked ( base_key : & str ) -> Result < Option < String > > {
598+ let entry = get_keyring_entry ( base_key) ?;
599+ match entry. get_password ( ) {
600+ Ok ( value) => Ok ( Some ( value) ) ,
601+ Err ( keyring:: Error :: NoEntry ) => Ok ( None ) ,
602+ Err ( e) => Err ( anyhow:: anyhow!( "Keyring error loading {}: {}" , base_key, e) ) ,
603+ }
604+ }
605+
606+ /// Non-Windows: Delete credential directly.
607+ #[ cfg( not( target_os = "windows" ) ) ]
608+ fn delete_credential_chunked ( base_key : & str ) -> Result < bool > {
609+ if let Ok ( entry) = get_keyring_entry ( base_key) {
610+ match entry. delete_credential ( ) {
611+ Ok ( ( ) ) => return Ok ( true ) ,
612+ Err ( keyring:: Error :: NoEntry ) => return Ok ( false ) ,
613+ Err ( e) => {
614+ return Err ( anyhow:: anyhow!(
615+ "Keyring error deleting {}: {}" ,
616+ base_key,
617+ e
618+ ) ) ;
619+ }
620+ }
621+ }
622+ Ok ( false )
623+ }
624+
408625/// Load authentication data from the system keyring.
626+ ///
627+ /// On Windows, this function handles credentials that may have been split into
628+ /// multiple chunks due to the 2560-byte BLOB size limit in Windows Credential Manager.
409629pub fn load_from_keyring ( ) -> Result < Option < SecureAuthData > > {
410- // Load metadata first
630+ // Load metadata first (metadata is always small enough to fit in a single entry)
411631 let metadata_entry = get_keyring_entry ( KEYRING_KEY_METADATA ) ?;
412632
413633 let metadata: AuthData = match metadata_entry. get_password ( ) {
@@ -416,29 +636,29 @@ pub fn load_from_keyring() -> Result<Option<SecureAuthData>> {
416636 Err ( e) => return Err ( anyhow:: anyhow!( "Keyring error: {e}" ) ) ,
417637 } ;
418638
419- // Load actual secrets based on metadata
639+ // Load actual secrets based on metadata (using chunked loading for Windows compatibility)
420640 let api_key = if metadata. has_api_key {
421- match get_keyring_entry ( KEYRING_KEY_API ) ? . get_password ( ) {
422- Ok ( key) => Some ( SecretString :: from ( key) ) ,
423- Err ( _) => None ,
641+ match load_credential_chunked ( KEYRING_KEY_API ) {
642+ Ok ( Some ( key) ) => Some ( SecretString :: from ( key) ) ,
643+ Ok ( None ) | Err ( _) => None ,
424644 }
425645 } else {
426646 None
427647 } ;
428648
429649 let access_token = if metadata. has_access_token {
430- match get_keyring_entry ( KEYRING_KEY_ACCESS ) ? . get_password ( ) {
431- Ok ( token) => Some ( SecretString :: from ( token) ) ,
432- Err ( _) => None ,
650+ match load_credential_chunked ( KEYRING_KEY_ACCESS ) {
651+ Ok ( Some ( token) ) => Some ( SecretString :: from ( token) ) ,
652+ Ok ( None ) | Err ( _) => None ,
433653 }
434654 } else {
435655 None
436656 } ;
437657
438658 let refresh_token = if metadata. has_refresh_token {
439- match get_keyring_entry ( KEYRING_KEY_REFRESH ) ? . get_password ( ) {
440- Ok ( token) => Some ( SecretString :: from ( token) ) ,
441- Err ( _) => None ,
659+ match load_credential_chunked ( KEYRING_KEY_REFRESH ) {
660+ Ok ( Some ( token) ) => Some ( SecretString :: from ( token) ) ,
661+ Ok ( None ) | Err ( _) => None ,
442662 }
443663 } else {
444664 None
@@ -454,27 +674,28 @@ pub fn load_from_keyring() -> Result<Option<SecureAuthData>> {
454674 } ) )
455675}
456676
677+ /// Save authentication data to the system keyring.
678+ ///
679+ /// On Windows, this function automatically splits large credentials (like OAuth JWT tokens)
680+ /// into multiple chunks to work around the 2560-byte BLOB size limit in Windows Credential Manager.
457681fn save_to_keyring ( data : & SecureAuthData ) -> Result < ( ) > {
458- // Save secrets
682+ // Save secrets (using chunked saving for Windows compatibility)
459683 if let Some ( ref api_key) = data. api_key {
460- get_keyring_entry ( KEYRING_KEY_API ) ?
461- . set_password ( api_key. expose_secret ( ) )
684+ save_credential_chunked ( KEYRING_KEY_API , api_key. expose_secret ( ) )
462685 . context ( "Failed to save API key to keyring" ) ?;
463686 }
464687
465688 if let Some ( ref access_token) = data. access_token {
466- get_keyring_entry ( KEYRING_KEY_ACCESS ) ?
467- . set_password ( access_token. expose_secret ( ) )
689+ save_credential_chunked ( KEYRING_KEY_ACCESS , access_token. expose_secret ( ) )
468690 . context ( "Failed to save access token to keyring" ) ?;
469691 }
470692
471693 if let Some ( ref refresh_token) = data. refresh_token {
472- get_keyring_entry ( KEYRING_KEY_REFRESH ) ?
473- . set_password ( refresh_token. expose_secret ( ) )
694+ save_credential_chunked ( KEYRING_KEY_REFRESH , refresh_token. expose_secret ( ) )
474695 . context ( "Failed to save refresh token to keyring" ) ?;
475696 }
476697
477- // Save metadata
698+ // Save metadata (always small enough to fit in a single entry)
478699 let metadata = data. to_metadata ( ) ;
479700 let metadata_json = serde_json:: to_string ( & metadata) . context ( "Failed to serialize metadata" ) ?;
480701
@@ -485,22 +706,25 @@ fn save_to_keyring(data: &SecureAuthData) -> Result<()> {
485706 Ok ( ( ) )
486707}
487708
709+ /// Delete authentication data from the system keyring.
710+ ///
711+ /// On Windows, this function also cleans up any chunk entries that may have been
712+ /// created for large credentials.
488713fn delete_from_keyring ( ) -> Result < bool > {
489714 let mut deleted = false ;
490715
491- // Delete all keyring entries
492- for key in & [
493- KEYRING_KEY_API ,
494- KEYRING_KEY_ACCESS ,
495- KEYRING_KEY_REFRESH ,
496- KEYRING_KEY_METADATA ,
497- ] {
498- if let Ok ( entry) = get_keyring_entry ( key) {
499- match entry. delete_credential ( ) {
500- Ok ( ( ) ) => deleted = true ,
501- Err ( keyring:: Error :: NoEntry ) => { }
502- Err ( e) => return Err ( anyhow:: anyhow!( "Keyring error: {e}" ) ) ,
503- }
716+ // Delete all keyring entries (using chunked deletion for Windows compatibility)
717+ // This ensures any chunk entries are also cleaned up
718+ for key in & [ KEYRING_KEY_API , KEYRING_KEY_ACCESS , KEYRING_KEY_REFRESH ] {
719+ deleted |= delete_credential_chunked ( key) ?;
720+ }
721+
722+ // Delete metadata (always a single entry)
723+ if let Ok ( entry) = get_keyring_entry ( KEYRING_KEY_METADATA ) {
724+ match entry. delete_credential ( ) {
725+ Ok ( ( ) ) => deleted = true ,
726+ Err ( keyring:: Error :: NoEntry ) => { }
727+ Err ( e) => return Err ( anyhow:: anyhow!( "Keyring error: {e}" ) ) ,
504728 }
505729 }
506730
0 commit comments