/*******************************************************************************
* Copyright 2012-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use
* this file except in compliance with the License. A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file.
* This file 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.
* *****************************************************************************
*
* AWS Tools for Windows (TM) PowerShell (TM)
*
*/
using System;
using System.Linq;
using System.Management.Automation;
using System.IO;
using Amazon.PowerShell.Common;
using Amazon.EC2.Model;
using Amazon.Runtime.Internal.Settings;
using Amazon.EC2;
using Amazon.Runtime;
using Amazon.Runtime.CredentialManagement.Internal;
using Amazon.Runtime.CredentialManagement;
namespace Amazon.PowerShell.Cmdlets.EC2
{
///
///
/// Retrieves the encrypted administrator password for the instances running Windows and optionally decrypts it.
///
///
/// When running on Windows with the desktop version of PowerShell if the -Decrypt switch is specified the cmdlet
/// can attempt to auto-discover the name of the keypair that was used to launch the instance, and inspects the
/// configuration store of the AWS Toolkit for Visual Studio to determine if the corresponding keypair data needed
/// to decrypt the password is available locally. If it is the password will be decrypted without needing to specify
/// the location of the Pem file.
///
///
/// On platforms other than Windows, or when running PowerShell Core on Windows, the configuration store of the AWS
/// Toolkit for Visual Studio is not available. In these situations the location of a Pem file containing the data
/// needed to decrypt the password can be supplied to the -PemFile parameter.
///
///
/// Note that if the -PemFile parameter is supplied (on any platform), the cmdlet automatically assumes that -Decrypt
/// is set.
///
///
[Cmdlet("Get", "EC2PasswordData", DefaultParameterSetName = AutoInspectForPemFile)]
[OutputType("PasswordData", "string")]
[AWSCmdlet("Calls the Amazon Elastic Compute Cloud GetPasswordData API operation.", Operation = new[] { "GetPasswordData" })]
[AWSCmdletOutput("PasswordData",
"If -Decrypt or -PemFile are not specified, returns a string containing the encrypted password for later decryption.",
"The service response (type Amazon.EC2.Model.GetPasswordDataResponse) is added to the cmdlet entry in the $AWSHistory stack."
)]
[AWSCmdletOutput("string", "If -Decrypt or -PemFile is specified, the decrypted password.")]
public class GetEC2PasswordDataCmdlet : AmazonEC2ClientCmdlet, IExecutor
{
private const string AutoInspectForPemFile = "AutoInspectForPemFile";
private const string ManuallySupplyPemFile = "ManuallySupplyPemFile";
#region Parameter InstanceId
///
/// The ID of the instance for which to get the password.
///
[Parameter(Position=0, ValueFromPipelineByPropertyName=true, ParameterSetName = AutoInspectForPemFile, Mandatory=true)]
[Parameter(Position = 0, ValueFromPipelineByPropertyName = true, ParameterSetName = ManuallySupplyPemFile, Mandatory=true)]
[Amazon.PowerShell.Common.AWSRequiredParameter]
public System.String InstanceId { get; set; }
#endregion
#region Parameter Decrypt
///
///
/// If specified the instance password is decrypted and emitted to the pipeline as a string.
///
///
/// Note: If the -Pem File parameter is used this switch is assumed to be set. It is included
/// in both parameter sets for this cmdlet for legacy, non-breaking change reasons.
///
///
[Parameter(ParameterSetName = AutoInspectForPemFile, ValueFromPipelineByPropertyName = true)]
[Parameter(ParameterSetName = ManuallySupplyPemFile, ValueFromPipelineByPropertyName = true)]
public SwitchParameter Decrypt { get; set; }
#endregion
#region Parameter PemFile
///
///
/// The name of a .pem file containing the key materials corresponding to the keypair
/// used to launch the instance. This will be used to decrypt the password data.
///
///
/// If -PemFile is specified, then -Decrypt is assumed.
///
///
[Parameter(ParameterSetName = ManuallySupplyPemFile, Mandatory = true, ValueFromPipelineByPropertyName = true)]
[Amazon.PowerShell.Common.AWSRequiredParameter]
public System.String PemFile { get; set; }
#endregion
protected override void ProcessRecord()
{
base.ProcessRecord();
var context = new CmdletContext
{
InstanceId = this.InstanceId,
Decrypt = this.Decrypt.IsPresent
};
// specifying a pem file implies the user wants the password decrypted
if (!string.IsNullOrEmpty(this.PemFile))
{
context.Decrypt = true;
context.PemFile = PSHelpers.PSPathToAbsolute(this.SessionState.Path, this.PemFile.Trim());
}
var output = Execute(context) as CmdletOutput;
ProcessOutput(output);
}
#region IExecutor Members
public object Execute(ExecutorContext context)
{
var cmdletContext = context as CmdletContext;
var request = new GetPasswordDataRequest();
if (cmdletContext.InstanceId != null)
{
request.InstanceId = cmdletContext.InstanceId;
}
var client = Client ?? CreateClient(_CurrentCredentials, _RegionEndpoint);
CmdletOutput output;
try
{
var response = CallAWSServiceOperation(client, request);
if (string.IsNullOrEmpty(response.PasswordData))
{
var msg = string.Format(
"Password data is not yet available for instance {0}.\n\nPassword generation and encryption can sometimes take more than 30 minutes. Please wait at least 5 minutes after launching an instance before trying to retrieve the generated password.",
cmdletContext.InstanceId);
this.WriteWarning(msg);
return new CmdletOutput
{
ServiceResponse = response
};
}
if (!cmdletContext.Decrypt)
{
output = new CmdletOutput
{
PipelineOutput = response.PasswordData,
ServiceResponse = response
};
}
else
{
output = new CmdletOutput
{
PipelineOutput = string.IsNullOrEmpty(cmdletContext.PemFile)
? DecryptViaPemDiscovery(cmdletContext.InstanceId, response, ProfileName, ProfileLocation)
: DecryptViaPemFile(cmdletContext.PemFile, response)
};
}
}
catch (Exception e)
{
output = new CmdletOutput { ErrorResponse = e };
}
return output;
}
///
/// Retrieves the name of the keypair used on launch from the instance, then inspects
/// the local AWS Toolkit store for matching account & keypair data before decrypting
/// the password data.
///
/// The instance the user wants the password for
/// Encrypted password data retrieved from the instance
/// The location of the ini-format credential file.
/// The decrypted password
string DecryptViaPemDiscovery(string instanceId, GetPasswordDataResponse passwordDataResponse, string profileName, string profileLocation)
{
string decryptedPassword = null;
try
{
WriteVerbose(string.Format("Retrieving keyname from running instance {0}", instanceId));
var request = new DescribeInstancesRequest();
request.InstanceIds.Add(instanceId);
var response = CallAWSServiceOperation(Client, request);
string keyName = response.Reservations[0].Instances[0].KeyName;
WriteVerbose(string.Format("Retrieved keyname {0}, decrypting password data", keyName));
string accountSettingsKey = LookupAccountSettingsKey(this._CurrentCredentials.GetCredentials().AccessKey, profileName, profileLocation);
if (string.IsNullOrEmpty(accountSettingsKey))
throw new InvalidOperationException("Unable to determine stored account settings from access key");
if (ToolkitKeyPairHelper.DoesPrivateKeyExist(accountSettingsKey, this._RegionEndpoint.SystemName, keyName))
{
string privateKey = ToolkitKeyPairHelper.GetPrivateKey(accountSettingsKey, this._RegionEndpoint.SystemName, keyName);
decryptedPassword = passwordDataResponse.GetDecryptedPassword(privateKey);
}
}
catch (Exception e)
{
this.ThrowTerminatingError(new ErrorRecord(e, "InvalidOperationException", ErrorCategory.InvalidOperation, this));
}
return decryptedPassword;
}
///
/// Loads the specified .pem file and uses it to decrypt the password data returned
/// from the instance.
///
/// The full path to the .pem file to use
/// Encrypted password data retrieved from the instance
/// The decrypted password
string DecryptViaPemFile(string pemFile, GetPasswordDataResponse passwordDataResponse)
{
string decryptedPassword = null;
try
{
if (!File.Exists(pemFile))
throw new ArgumentException("Specified .pem file does not exist");
string privateKey = null;
using (var fs = File.OpenRead(pemFile))
using (var reader = new StreamReader(fs))
{
privateKey = reader.ReadToEnd();
}
decryptedPassword = passwordDataResponse.GetDecryptedPassword(privateKey);
}
catch (Exception e)
{
this.ThrowTerminatingError(new ErrorRecord(e, "InvalidOperationException", ErrorCategory.InvalidOperation, this));
}
return decryptedPassword;
}
///
/// Walks the credentials store to find the matching account settings for the specified
/// access key
///
/// Access key to serach by.
/// ProfileName to search by.
/// The location of the ini-format credential file.
///
string LookupAccountSettingsKey(string accessKey, string profileName, string profileLocation)
{
CredentialProfile profile;
if (string.IsNullOrEmpty(profileName))
{
// no profile name so go by the access key
profile = SettingsStore.ListProfiles(profileLocation).Where(
p => p.CanCreateAWSCredentials && string.Equals(p.Options.AccessKey, accessKey, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
}
else
{
// use profile name since we have it
SettingsStore.TryGetProfile(profileName, profileLocation, out profile);
}
if (profile != null)
return CredentialProfileUtils.GetUniqueKey(profile);
else
return null;
}
public ExecutorContext CreateContext()
{
return new CmdletContext();
}
#endregion
#region AWS Service Operation Call
private Amazon.EC2.Model.GetPasswordDataResponse CallAWSServiceOperation(IAmazonEC2 client, Amazon.EC2.Model.GetPasswordDataRequest request)
{
Utils.Common.WriteVerboseEndpointMessage(this, client.Config, "Amazon EC2", "GetPasswordData");
try
{
#if DESKTOP
return client.GetPasswordData(request);
#elif CORECLR
return client.GetPasswordDataAsync(request).GetAwaiter().GetResult();
#else
#error "Unknown build edition"
#endif
}
catch (AmazonServiceException exc)
{
var webException = exc.InnerException as System.Net.WebException;
if (webException != null)
{
throw new Exception(Utils.Common.FormatNameResolutionFailureMessage(client.Config, webException.Message), webException);
}
throw;
}
}
private Amazon.EC2.Model.DescribeInstancesResponse CallAWSServiceOperation(IAmazonEC2 client, Amazon.EC2.Model.DescribeInstancesRequest request)
{
Utils.Common.WriteVerboseEndpointMessage(this, client.Config, "Amazon EC2", "DescribeInstances");
#if DESKTOP
return client.DescribeInstances(request);
#elif CORECLR
return client.DescribeInstancesAsync(request).GetAwaiter().GetResult();
#else
#error "Unknown build edition"
#endif
}
#endregion
// temp copy of ec2 plugins helper code, until we can (perhaps) get it moved into
// toolkit utils
internal static class ToolkitKeyPairHelper
{
public static bool DoesPrivateKeyExist(string accountSettingsKey, string region, string keyPairName)
{
string fullpath = GetFullPath(accountSettingsKey, region, keyPairName);
return File.Exists(fullpath);
}
public static string GetPrivateKey(string accountSettingsKey, string region, string keyPairName)
{
try
{
string fullpath = GetFullPath(accountSettingsKey, region, keyPairName);
if (!File.Exists(fullpath))
return null;
string encryptedPrivateKey = null;
using (var fs = File.OpenRead(fullpath))
using (var reader = new StreamReader(fs))
{
encryptedPrivateKey = reader.ReadToEnd();
}
return UserCrypto.Decrypt(encryptedPrivateKey);
}
catch (Exception e)
{
throw new InvalidOperationException("Failed to read private key from settings folder", e);
}
}
static string GetFullPath(string accountId, string region, string keyPairName)
{
string keyLocation = GetDirectory(accountId, region, keyPairName);
string fullpath = string.Format(@"{0}\{1}.pem.encrypted", keyLocation, keyPairName);
return fullpath;
}
static string GetDirectory(string accountId, string region, string keyPairName)
{
string settingsFolder = PersistenceManager.GetSettingsStoreFolder();
string keyLocation = string.Format(@"{0}\keypairs\{1}\{2}", settingsFolder, accountId, region);
if (!Directory.Exists(keyLocation))
Directory.CreateDirectory(keyLocation);
return keyLocation;
}
}
internal class CmdletContext : ExecutorContext
{
public String InstanceId { get; set; }
public bool Decrypt { get; set; }
public string PemFile { get; set; }
}
}
}