public async Task CreateWindowsCredentialsAsync()

in sources/Google.Solutions.Apis/Compute/WindowsCredentialGenerator.cs [116:292]


        public async Task<NetworkCredential> CreateWindowsCredentialsAsync(
            InstanceLocator instanceRef,
            string username,
            UserFlags userType,
            CancellationToken token)
        {
            using (ApiTraceSource.Log.TraceMethod().WithParameters(instanceRef, username))
            using (var rsa = new RSACng(RsaKeySize))
            {
                var keyParameters = rsa.ExportParameters(false);

                var requestPayload = new RequestPayload()
                {
                    ExpireOn = DateTime.UtcNow.AddMinutes(5),
                    Username = username,
                    Email = username,
                    Modulus = Convert.ToBase64String(keyParameters.Modulus),
                    Exponent = Convert.ToBase64String(keyParameters.Exponent),
                    AddToAdministrators = userType.HasFlag(UserFlags.AddToAdministrators),
                    HashFunction = DefaultHashFunction
                };

                //
                // Send the request to the instance via a special metadata entry.
                //
                try
                {
                    var requestJson = JsonConvert.SerializeObject(requestPayload);
                    await this.computeClient.UpdateMetadataAsync(
                            instanceRef,
                            existingMetadata =>
                            {
                                existingMetadata.Add(new Metadata()
                                {
                                    Items = new[]
                                    {
                                        new Metadata.ItemsData()
                                        {
                                            Key = MetadataKey,
                                            Value = requestJson
                                        }
                                    }
                                });
                            },
                            token)
                        .ConfigureAwait(false);
                }
                catch (ResourceNotFoundException e)
                {
                    ApiTraceSource.Log.TraceVerbose("Instance does not exist: {0}", e.Message);

                    throw new WindowsCredentialCreationFailedException(
                        $"Instance {instanceRef.Name} was not found.");
                }
                catch (ResourceAccessDeniedException e)
                {
                    ApiTraceSource.Log.TraceVerbose(
                        "Setting request payload metadata failed with 403: {0}",
                        e.FullMessage());

                    //
                    // Setting metadata failed due to lack of permissions. Note that
                    // the Error object is not always populated, hence the OR filter.
                    //

                    throw new WindowsCredentialCreationFailedException(
                        "You do not have sufficient permissions to reset a Windows password. " +
                        "You need the 'Service Account User' and " +
                        "'Compute Instance Admin' roles (or equivalent custom roles) " +
                        "to perform this action.",
                        HelpTopics.PermissionsToResetWindowsUser);
                }
                catch (GoogleApiException e) when (e.IsBadRequestCausedByServiceAccountAccessDenied())
                {
                    ApiTraceSource.Log.TraceVerbose(
                        "Setting request payload metadata failed with 400: {0} ({1})",
                        e.Message,
                        e.Error?.Errors.EnsureNotNull().Select(er => er.Reason).FirstOrDefault());

                    //
                    // This error happens if the user has the necessary
                    // permissions on the VM, but lacks ActAs permission on
                    // the associated service account.
                    //

                    throw new WindowsCredentialCreationFailedException(
                        "You do not have sufficient permissions to reset a Windows password. " +
                        "Because this VM instance uses a service account, you also need the " +
                        "'Service Account User' role.",
                        HelpTopics.PermissionsToResetWindowsUser);
                }

                //
                // Read response from serial port.
                //
                using (var serialPortStream = this.computeClient.GetSerialPortOutput(
                    instanceRef,
                    SerialPort))
                {
                    //
                    // It is rare, but sometimes a single JSON can be split over multiple
                    // API reads. Therefore, maintain a buffer.
                    //

                    var logBuffer = new StringBuilder(64 * 1024);
                    while (true)
                    {
                        ApiTraceSource.Log.TraceVerbose("Waiting for agent to supply response...");

                        token.ThrowIfCancellationRequested();

                        var logDelta = await serialPortStream.ReadAsync(token).ConfigureAwait(false);
                        if (string.IsNullOrEmpty(logDelta))
                        {
                            // Reached end of stream, wait and try again.
                            await Task.Delay(500, token).ConfigureAwait(false);
                            continue;
                        }

                        logBuffer.Append(logDelta);

                        //
                        // NB. Old versions of the Windows guest agent wrongly added a '\'
                        // before every '/' in base64-encoded data. This affects the search
                        // for the modulus.
                        //

                        var response = logBuffer.ToString()
                            .Split('\n')
                            .Where(line => line.Contains(requestPayload.Modulus) ||
                                           line.Replace("\\/", "/").Contains(requestPayload.Modulus))
                            .FirstOrDefault();
                        if (response == null)
                        {
                            //
                            // That was not the output we are looking for, keep reading.
                            //
                            continue;
                        }

                        var responsePayload = JsonConvert.DeserializeObject<ResponsePayload>(response);
                        if (responsePayload == null ||
                            !string.IsNullOrEmpty(responsePayload.ErrorMessage))
                        {
                            throw new WindowsCredentialCreationFailedException(
                                responsePayload?.ErrorMessage ?? "The response is empty");
                        }

                        //
                        // Old versions of the Windows guest agent unconditionally used
                        // OaepSHA1. Current versions vary the padding based on the
                        // hash function passed in the request, and echo the hash function
                        // in the response.
                        //
                        // If the response contains the hash function we requested, then
                        // we know it's a current version and we can use the appropriate
                        // OAEP padding. If the response contains no hash function field,
                        // then we're dealing with an old version and need to fall back
                        // to OaepSHA1.
                        //

                        var padding = responsePayload.HashFunction == DefaultHashFunction
                            ? DefaultEncryptionPadding
                            : RSAEncryptionPadding.OaepSHA1;

                        var password = rsa.Decrypt(
                            Convert.FromBase64String(responsePayload.EncryptedPassword),
                            padding);

                        return new NetworkCredential(
                            username,
                            new UTF8Encoding().GetString(password),
                            null);
                    }
                }
            }
        }