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);
}
}
}