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