private Libssh2AuthenticatedSession AuthenticateWithKeyboard()

in sources/Google.Solutions.Ssh/Native/Libssh2ConnectedSession.cs [278:441]


        private Libssh2AuthenticatedSession AuthenticateWithKeyboard(
            ISshCredential credential,
            IKeyboardInteractiveHandler keyboardHandler,
            string defaultPromptName)
        {
            this.session.Handle.CheckCurrentThreadOwnsHandle();
            Precondition.ExpectNotNull(credential, nameof(credential));
            Precondition.ExpectNotNull(keyboardHandler, nameof(keyboardHandler));

            Exception? interactiveCallbackException = null;

            void InteractiveCallback(
                IntPtr namePtr,
                int nameLength,
                IntPtr instructionPtr,
                int instructionLength,
                int numPrompts,
                IntPtr promptsPtr,
                IntPtr responsesPtr,
                IntPtr context)
            {
                var name = NativeMethods.PtrToString(
                    namePtr,
                    nameLength,
                    Encoding.UTF8);
                var instruction = NativeMethods.PtrToString(
                    instructionPtr,
                    nameLength,
                    Encoding.UTF8);
                var prompts = NativeMethods.PtrToStructureArray<
                        NativeMethods.LIBSSH2_USERAUTH_KBDINT_PROMPT>(
                    promptsPtr,
                    numPrompts);

                SshEventSource.Log.KeyboardInteractivePromptReceived(name, instruction);

                //
                // NB. libssh2 allocates the responses structure for us, but frees
                // the embedded text strings using its allocator.
                // 
                // NB. libssh2 assumes text to be encoded in UTF-8.
                //
                Debug.Assert(Libssh2Session.Alloc != null);

                var responses = new NativeMethods.LIBSSH2_USERAUTH_KBDINT_RESPONSE[prompts.Length];
                for (var i = 0; i < prompts.Length; i++)
                {
                    var promptText = NativeMethods.PtrToString(
                        prompts[i].TextPtr,
                        prompts[i].TextLength,
                        Encoding.UTF8);

                    SshTraceSource.Log.TraceVerbose("Keyboard/interactive prompt: {0}", promptText);

                    //
                    // NB. Name and instruction are often null or empty:
                    //
                    //  - OS Login 2SV sets the prompt text, but leaves name and
                    //    instruction empty.
                    //  - When keyboard-interactive is used to handle password-
                    //    authentication, the prompt text contains "Password:",
                    //    and name and instruction are empty.
                    //
                    string? responseText = null;
                    try
                    {
                        responseText = keyboardHandler.Prompt(
                            name ?? defaultPromptName,
                            instruction ?? string.Empty,
                            promptText ?? string.Empty,
                            prompts[i].Echo != 0);
                    }
                    catch (Exception e)
                    {
                        SshTraceSource.Log.TraceError(
                            "Authentication callback threw exception", e);

                        SshEventSource.Log.KeyboardInteractiveChallengeAborted(e.FullMessage());

                        //
                        // Don't let the exception escape into unmanaged code,
                        // instead return null and let the enclosing method
                        // rethrow the exception once we're back on a managed
                        // callstack.
                        //
                        interactiveCallbackException = e;
                    }

                    responses[i] = new NativeMethods.LIBSSH2_USERAUTH_KBDINT_RESPONSE();
                    if (responseText == null)
                    {
                        responses[i].TextLength = 0;
                        responses[i].TextPtr = IntPtr.Zero;
                    }
                    else
                    {
                        var responseTextBytes = Encoding.UTF8.GetBytes(responseText);
                        responses[i].TextLength = responseTextBytes.Length;
                        responses[i].TextPtr = Libssh2Session.Alloc!(
                            new IntPtr(responseTextBytes.Length),
                            IntPtr.Zero);
                        Marshal.Copy(
                            responseTextBytes,
                            0,
                            responses[i].TextPtr,
                            responseTextBytes.Length);
                    }
                }

                NativeMethods.StructureArrayToPtr(
                    responsesPtr,
                    responses);
            }

            using (SshTraceSource.Log.TraceMethod().WithParameters(credential.Username))
            {
                var result = LIBSSH2_ERROR.NONE;

                //
                // Temporarily change the timeout since we must give the
                // user some time to react.
                //
                using (this.session.WithTimeout(this.session.KeyboardInteractivePromptTimeout))
                {
                    //
                    // Retry to account for wrong user input.
                    //
                    for (var retry = 0; retry < KeyboardInteractiveRetries; retry++)
                    {
                        SshEventSource.Log.KeyboardInteractiveAuthenticationInitiated();

                        result = (LIBSSH2_ERROR)NativeMethods.libssh2_userauth_keyboard_interactive_ex(
                            this.session.Handle,
                            credential.Username,
                            credential.Username.Length,
                            InteractiveCallback,
                            IntPtr.Zero);

                        if (result == LIBSSH2_ERROR.NONE)
                        {
                            break;
                        }
                        else if (interactiveCallbackException != null)
                        {
                            //
                            // Restore exception thrown in callback.
                            //
                            throw interactiveCallbackException;
                        }
                    }
                }

                if (result == LIBSSH2_ERROR.NONE)
                {
                    SshEventSource.Log.KeyboardInteractiveAuthenticationCompleted();

                    return new Libssh2AuthenticatedSession(this.session);
                }
                else
                {
                    throw this.session.CreateException(result);
                }
            }
        }