@@ -35,7 +35,9 @@ internal enum LibSSH2TunnelFactory {
3535
3636 // MARK: - Global Init
3737
38- private static let initialized : Bool = {
38+ /// libssh2's own header says `libssh2_init` uses global state and must not be called
39+ /// concurrently, so every entry point in the process goes through this one lazy static.
40+ internal static let initialized : Bool = {
3941 libssh2_init ( 0 )
4042 return true
4143 } ( )
@@ -501,8 +503,7 @@ internal enum LibSSH2TunnelFactory {
501503 buildKeyFileAuthenticator (
502504 keyPath: keyPath,
503505 providedPassphrase: credentials. keyPassphrase,
504- resolved: resolved,
505- canPrompt: true
506+ resolved: resolved
506507 )
507508 }
508509 authenticators. append ( KeyboardInteractiveAuthenticator (
@@ -513,28 +514,28 @@ internal enum LibSSH2TunnelFactory {
513514 return CompositeAuthenticator ( authenticators: authenticators)
514515
515516 case . sshAgent:
517+ // The agent is the credential, so there is no key-file fallback: authenticating with a
518+ // key the user never chose put TablePro's own passphrase prompt over an agent that had
519+ // simply not been reached (#2583). Keyboard-interactive stays, being a second factor the
520+ // same server asked for rather than another credential.
516521 let socketPath : String ? = resolved. agentSocketPath. isEmpty
517522 ? nil
518523 : SSHPathUtilities . expandTilde ( resolved. agentSocketPath)
519524
520- var authenticators : [ any SSHAuthenticator ] = [ AgentAuthenticator ( socketPath: socketPath) ]
521-
522- for keyPath in effectiveKeyPaths ( for: resolved) {
523- authenticators. append ( buildKeyFileAuthenticator (
524- keyPath: keyPath,
525- providedPassphrase: credentials. keyPassphrase,
526- resolved: resolved,
527- canPrompt: true
528- ) )
529- }
530-
531- authenticators. append ( KeyboardInteractiveAuthenticator (
532- password: nil ,
533- totpProvider: buildTOTPProvider ( config: config, credentials: credentials) ,
534- promptProvider: promptProvider
535- ) )
536-
537- return CompositeAuthenticator ( authenticators: authenticators)
525+ return CompositeAuthenticator (
526+ authenticators: [
527+ AgentAuthenticator ( socketPath: socketPath, socketOrigin: resolved. agentSocketOrigin) ,
528+ KeyboardInteractiveAuthenticator (
529+ password: nil ,
530+ totpProvider: buildTOTPProvider ( config: config, credentials: credentials) ,
531+ promptProvider: promptProvider
532+ ) ,
533+ ] ,
534+ endsChainOn: Set (
535+ AgentSocketOrigin . allCases. map ( AuthFailureReason . agentUnavailable)
536+ + AgentSocketOrigin. allCases. map ( AuthFailureReason . agentNoIdentities)
537+ )
538+ )
538539
539540 case . keyboardInteractive:
540541 return KeyboardInteractiveAuthenticator (
@@ -562,19 +563,16 @@ internal enum LibSSH2TunnelFactory {
562563 . filter { FileManager . default. isReadableFile ( atPath: $0) }
563564 }
564565
565- /// Passphrase resolution is deferred to auth time (not build time) so
566- /// that, when this authenticator is used as an agent fallback, the user
567- /// is only prompted if the agent actually fails.
566+ /// Passphrase resolution is deferred to auth time (not build time) so that a key later in
567+ /// the chain only prompts once the ones before it have actually been refused.
568568 private static func buildKeyFileAuthenticator(
569569 keyPath: String ,
570570 providedPassphrase: String ? ,
571- resolved: ResolvedSSHTarget ,
572- canPrompt: Bool
571+ resolved: ResolvedSSHTarget
573572 ) -> any SSHAuthenticator {
574573 KeyFileAuthenticator (
575574 keyPath: keyPath,
576575 providedPassphrase: providedPassphrase,
577- canPrompt: canPrompt,
578576 useKeychain: resolved. useKeychain,
579577 addKeysToAgent: resolved. addKeysToAgent
580578 )
@@ -586,7 +584,6 @@ internal enum LibSSH2TunnelFactory {
586584 private struct KeyFileAuthenticator : SSHAuthenticator {
587585 let keyPath : String
588586 let providedPassphrase : String ?
589- let canPrompt : Bool
590587 let useKeychain : Bool
591588 let addKeysToAgent : Bool
592589
@@ -617,9 +614,7 @@ internal enum LibSSH2TunnelFactory {
617614 }
618615 }
619616
620- // 2. Prompt the user if allowed (key is encrypted, no stored passphrase)
621- guard canPrompt else { throw SSHTunnelError . authenticationFailed ( reason: . privateKey) }
622-
617+ // 2. Prompt the user (key is encrypted, no stored passphrase)
623618 let provider = PromptPassphraseProvider ( keyPath: expandedPath)
624619 guard let promptResult = provider. providePassphrase ( ) else {
625620 throw SSHTunnelError . authenticationFailed ( reason: . privateKey)
@@ -668,7 +663,6 @@ internal enum LibSSH2TunnelFactory {
668663 KeyFileAuthenticator (
669664 keyPath: path,
670665 providedPassphrase: nil ,
671- canPrompt: true ,
672666 useKeychain: resolved. useKeychain,
673667 addKeysToAgent: resolved. addKeysToAgent
674668 )
@@ -678,12 +672,11 @@ internal enum LibSSH2TunnelFactory {
678672 : CompositeAuthenticator ( authenticators: authenticators)
679673 case . sshAgent:
680674 let socketPath : String ? = resolved. agentSocketPath. isEmpty ? nil : resolved. agentSocketPath
681- let agent = AgentAuthenticator ( socketPath: socketPath)
675+ let agent = AgentAuthenticator ( socketPath: socketPath, socketOrigin : resolved . agentSocketOrigin )
682676 if !jumpHost. privateKeyPath. isEmpty {
683677 let keyAuth = KeyFileAuthenticator (
684678 keyPath: jumpHost. privateKeyPath,
685679 providedPassphrase: nil ,
686- canPrompt: true ,
687680 useKeychain: resolved. useKeychain,
688681 addKeysToAgent: resolved. addKeysToAgent
689682 )
0 commit comments