sources/Google.Solutions.IapDesktop.Extensions.Session/Protocol/SessionContextFactory.cs (291 lines of code) (raw):
//
// Copyright 2023 Google LLC
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
using Google.Solutions.Apis.Auth;
using Google.Solutions.Apis.Locator;
using Google.Solutions.Common.Security;
using Google.Solutions.Common.Util;
using Google.Solutions.IapDesktop.Application.Data;
using Google.Solutions.IapDesktop.Application.Windows;
using Google.Solutions.IapDesktop.Core.ClientModel.Transport;
using Google.Solutions.IapDesktop.Core.ObjectModel;
using Google.Solutions.IapDesktop.Core.ProjectModel;
using Google.Solutions.IapDesktop.Extensions.Session.Protocol.Rdp;
using Google.Solutions.IapDesktop.Extensions.Session.Protocol.Ssh;
using Google.Solutions.IapDesktop.Extensions.Session.Settings;
using Google.Solutions.IapDesktop.Extensions.Session.ToolWindows.Session;
using Google.Solutions.Platform.Security.Cryptography;
using Google.Solutions.Settings.Collection;
using Google.Solutions.Ssh;
using Google.Solutions.Ssh.Cryptography;
using System;
using System.Diagnostics;
using System.Globalization;
using System.Security;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Google.Solutions.IapDesktop.Extensions.Session.Protocol
{
public interface ISessionContextFactory
{
/// <summary>
/// Create a new SSH session context. The method might require UI
/// interactiion.
/// </summary>
Task<ISessionContext<ISshCredential, SshParameters>> CreateSshSessionContextAsync(
IProjectModelInstanceNode node,
CancellationToken cancellationToken);
/// <summary>
/// Create a new RDP session context. The method might require UI
/// interactiion.
/// </summary>
Task<ISessionContext<RdpCredential, RdpParameters>> CreateRdpSessionContextAsync(
IProjectModelInstanceNode node,
RdpCreateSessionFlags flags,
CancellationToken cancellationToken);
/// <summary>
/// Create a new RDP session context. The method might require UI
/// interactiion.
/// </summary>
Task<ISessionContext<RdpCredential, RdpParameters>> CreateRdpSessionContextAsync(
IapRdpUrl url,
CancellationToken cancellationToken);
}
[Flags]
public enum RdpCreateSessionFlags
{
None = 0,
ForcePasswordPrompt
}
[Service(typeof(ISessionContextFactory))]
public class SessionContextFactory : ISessionContextFactory
{
internal static readonly TimeSpan EphemeralKeyValidity = TimeSpan.FromDays(1);
private readonly IWin32Window window;
private readonly IAuthorization authorization;
private readonly IKeyStore keyStore;
private readonly IPlatformCredentialFactory credentialFactory;
private readonly IConnectionSettingsService settingsService;
private readonly IRepository<ISshSettings> sshSettingsRepository;
private readonly IIapTransportFactory iapTransportFactory;
private readonly IDirectTransportFactory directTransportFactory;
private readonly IRdpCredentialEditorFactory rdpCredentialEditor;
private readonly IRdpCredentialCallback rdpCredentialCallbackService;
public SessionContextFactory(
IMainWindow window,
IAuthorization authorization,
IKeyStore keyStoreAdapter,
IPlatformCredentialFactory credentialFactory,
IConnectionSettingsService settingsService,
IIapTransportFactory iapTransportFactory,
IDirectTransportFactory directTransportFactory,
IRdpCredentialEditorFactory rdpCredentialEditor,
IRdpCredentialCallback credentialCallbackService,
IRepository<ISshSettings> sshSettingsRepository)
{
this.window = window.ExpectNotNull(nameof(window));
this.authorization = authorization.ExpectNotNull(nameof(authorization));
this.keyStore = keyStoreAdapter.ExpectNotNull(nameof(keyStoreAdapter));
this.credentialFactory = credentialFactory.ExpectNotNull(nameof(credentialFactory));
this.settingsService = settingsService.ExpectNotNull(nameof(settingsService));
this.iapTransportFactory = iapTransportFactory.ExpectNotNull(nameof(iapTransportFactory));
this.directTransportFactory = directTransportFactory.ExpectNotNull(nameof(directTransportFactory));
this.rdpCredentialEditor = rdpCredentialEditor;
this.rdpCredentialCallbackService = credentialCallbackService.ExpectNotNull(nameof(credentialCallbackService));
this.sshSettingsRepository = sshSettingsRepository.ExpectNotNull(nameof(sshSettingsRepository));
}
//---------------------------------------------------------------------
// ISessionContextFactory.
//---------------------------------------------------------------------
private static RdpCredential CreateRdpCredentialFromSettings(
ConnectionSettings settings)
{
return new RdpCredential(
settings.RdpUsername.Value,
settings.RdpDomain.Value,
(SecureString?)settings.RdpPassword.Value);
}
private RdpContext CreateRdpContext(
InstanceLocator instance,
RdpCredential credential,
ConnectionSettings settings,
RdpParameters.ParameterSources sources)
{
var context = new RdpContext(
this.iapTransportFactory,
this.directTransportFactory,
instance,
credential,
sources);
context.Parameters.Port = (ushort)settings.RdpPort.Value;
context.Parameters.TransportType = settings.RdpTransport.Value;
context.Parameters.ConnectionTimeout = TimeSpan.FromSeconds(settings.RdpConnectionTimeout.Value);
context.Parameters.ConnectionBar = settings.RdpConnectionBar.Value;
context.Parameters.AuthenticationLevel = settings.RdpAuthenticationLevel.Value;
context.Parameters.ColorDepth = settings.RdpColorDepth.Value;
context.Parameters.AudioPlayback = settings.RdpAudioPlayback.Value;
context.Parameters.AudioInput = settings.RdpAudioInput.Value;
context.Parameters.NetworkLevelAuthentication = settings.RdpNetworkLevelAuthentication.Value;
context.Parameters.UserAuthenticationBehavior = settings.RdpAutomaticLogon.Value;
context.Parameters.RedirectClipboard = settings.RdpRedirectClipboard.Value;
context.Parameters.RedirectPrinter = settings.RdpRedirectPrinter.Value;
context.Parameters.RedirectSmartCard = settings.RdpRedirectSmartCard.Value;
context.Parameters.RedirectPort = settings.RdpRedirectPort.Value;
context.Parameters.RedirectDrive = settings.RdpRedirectDrive.Value;
context.Parameters.RedirectDevice = settings.RdpRedirectDevice.Value;
context.Parameters.RedirectWebAuthn = settings.RdpRedirectWebAuthn.Value;
context.Parameters.HookWindowsKeys = settings.RdpHookWindowsKeys.Value;
context.Parameters.RestrictedAdminMode = settings.RdpRestrictedAdminMode.Value;
context.Parameters.SessionType = settings.RdpSessionType.Value;
context.Parameters.DpiScaling = settings.RdpDpiScaling.Value;
context.Parameters.DesktopSize = settings.RdpDesktopSize.Value;
return context;
}
public async Task<ISessionContext<RdpCredential, RdpParameters>> CreateRdpSessionContextAsync(
IProjectModelInstanceNode node,
RdpCreateSessionFlags flags,
CancellationToken _)
{
node.ExpectNotNull(nameof(node));
Debug.Assert(node.IsRdpSupported());
var settings = this.settingsService.GetConnectionSettings(node);
var credentialEditor = this.rdpCredentialEditor.Edit(settings.TypedCollection);
if (flags.HasFlag(RdpCreateSessionFlags.ForcePasswordPrompt))
{
//
// Force a prompt, even though the settings might
// contain valid credentials.
//
credentialEditor.AllowSave = false;
credentialEditor.PromptForCredentials();
}
else
{
//
// Give the user a chance to amend credentials in case
// the settings don't contain any credentials yet.
//
Debug.Assert(credentialEditor.AllowSave);
await credentialEditor
.AmendCredentialsAsync(RdpCredentialGenerationBehavior.AllowIfNoCredentialsFound)
.ConfigureAwait(true);
if (credentialEditor.AllowSave)
{
settings.Save();
}
}
var instanceSettings = (ConnectionSettings)settings.TypedCollection;
var credential = CreateRdpCredentialFromSettings(instanceSettings);
return CreateRdpContext(
node.Instance,
credential,
instanceSettings,
RdpParameters.ParameterSources.Inventory);
}
public async Task<ISessionContext<RdpCredential, RdpParameters>> CreateRdpSessionContextAsync(
IapRdpUrl url,
CancellationToken cancellationToken)
{
url.ExpectNotNull(nameof(url));
var sources = RdpParameters.ParameterSources.Url;
var settings = this.settingsService.GetConnectionSettings(
url,
out var foundInInventory);
if (foundInInventory)
{
//
// This project/VM exists in the inventory, so the settings
// represent a merge of stored settings and URL-based
// settings.
//
sources |= RdpParameters.ParameterSources.Inventory;
}
if (url.TryGetParameter("CredentialCallbackUrl", out string callbackUrlRaw) &&
Uri.TryCreate(callbackUrlRaw, UriKind.Absolute, out var callbackUrl))
{
//
// Invoke callback to obtain credentials.
//
var credential = await this.rdpCredentialCallbackService
.GetCredentialsAsync(callbackUrl, cancellationToken)
.ConfigureAwait(false);
return CreateRdpContext(
url.Instance,
credential,
settings,
sources);
}
else
{
if (!url.TryGetParameter<RdpCredentialGenerationBehavior>(
"CredentialGenerationBehavior",
out var allowedBehavior))
{
allowedBehavior = RdpCredentialGenerationBehavior._Default;
}
//
// Show prompt, but don't persist any generated credentials.
//
var credentialEditor = this.rdpCredentialEditor.Edit(settings);
credentialEditor.AllowSave = false;
await credentialEditor
.AmendCredentialsAsync(allowedBehavior)
.ConfigureAwait(true);
return CreateRdpContext(
url.Instance,
CreateRdpCredentialFromSettings(settings),
settings,
sources);
}
}
public Task<ISessionContext<ISshCredential, SshParameters>> CreateSshSessionContextAsync(
IProjectModelInstanceNode node,
CancellationToken _)
{
node.ExpectNotNull(nameof(node));
Debug.Assert(node.IsSshSupported());
var sshSettings = this.sshSettingsRepository.GetSettings();
var settings = this.settingsService
.GetConnectionSettings(node)
.TypedCollection;
SshContext context;
if (settings.SshPublicKeyAuthentication.Value == SshPublicKeyAuthentication.Enabled)
{
//
// Use an asymmetric key pair for authentication, and
// authorize it automatically using whichever mechanism
// is appropriate for the instance.
//
IAsymmetricKeySigner signer;
TimeSpan validity;
if (sshSettings.UsePersistentKey.Value)
{
//
// Load persistent CNG key. This might pop up dialogs.
//
var keyName = new CngKeyName(
this.authorization.Session,
sshSettings.PublicKeyType.Value,
this.keyStore.Provider);
try
{
signer = AsymmetricKeySigner.Create(
this.keyStore.OpenKey(
this.window.Handle,
keyName.Value,
keyName.Type,
CngKeyUsages.Signing,
false),
true);
validity = TimeSpan.FromSeconds(sshSettings.PublicKeyValidity.Value);
}
catch (CryptographicException e) when (
e is KeyStoreUnavailableException ||
e is InvalidKeyContainerException)
{
throw new SessionException(
"Creating or opening the SSH key failed because the " +
"Windows CNG key container or key store is inaccessible.\n\n" +
"If the problem persists, go to Tools > Options > SSH and disable " +
"the option to use a persistent key for SSH authentication.",
HelpTopics.TroubleshootingSsh,
e);
}
}
else
{
//
// Use an ephemeral key and cap its validity.
//
signer = EphemeralKeySigners.Get(sshSettings.PublicKeyType.Value);
validity = EphemeralKeyValidity;
}
Debug.Assert(signer != null);
//
// Initialize a context and pass ownership of the key to it.
//
context = new SshContext(
this.iapTransportFactory,
this.directTransportFactory,
this.credentialFactory,
signer!,
node.Instance);
context.Parameters.PublicKeyValidity = validity;
context.Parameters.PreferredUsername = settings.SshUsername.Value;
}
else
{
//
// Use password for authentication.
//
var username = string.IsNullOrEmpty(settings.SshUsername.Value)
? LinuxUser.SuggestUsername(this.authorization)
: settings.SshUsername.Value;
context = new SshContext(
this.iapTransportFactory,
this.directTransportFactory,
new StaticPasswordCredential(
username!,
((SecureString?)settings.SshPassword.Value) ?? SecureStringExtensions.Empty),
node.Instance);
}
context.Parameters.Port = (ushort)settings.SshPort.Value;
context.Parameters.TransportType = settings.SshTransport.Value;
context.Parameters.ConnectionTimeout = TimeSpan.FromSeconds(settings.SshConnectionTimeout.Value);
context.Parameters.Language = sshSettings.EnableLocalePropagation.Value
? CultureInfo.CurrentUICulture
: null;
context.Parameters.EnableFileAccess = sshSettings.EnableFileAccess.Value;
return Task.FromResult<ISessionContext<ISshCredential, SshParameters>>(context);
}
}
}