I am using Powershell to request a password from a user if not provided, based upon another answer. I then pass the password (no pun intended) to some program, do-something.exe
. Rather than have an intermediate variable, I tried to convert the password to a normal string "inline":
[CmdletBinding()]
Param(
[Parameter(Mandatory, HelpMessage="password?")] [SecureString]$password
)
do-something password=${[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password))}
That doesn't work. I could only get it to work using a temporary, intermediate variable:
[CmdletBinding()]
Param(
[Parameter(Mandatory, HelpMessage="password?")] [SecureString]$password
)
$pwd=[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password))
do-something.exe password=$pwd
Did I make a mistake trying to evaluate the password inline when invoking do-something.exe
? How can this be done?
${...}
is a variable reference, and whatever ...
is is taken verbatim as a variable name.
{...}
is typically not necessary, but is required in two cases: (a) if a variable name contains special characters and/or (b) in the context of an expandable string ("..."
), to disambiguate the variable name from subsequent characters - see this answerIn order to embed an expression or command as part of an argument, use $(...)
, the subexpression operator, and preferably enclose the entire argument in "..."
- that way, the entire argument is unambiguously passed as a single argument, whereas an unquoted token that starts with a $(...)
subexpression would be passed as (at least) two arguments (see this answer).
(...)
, the grouping operator is sufficient and usually preferable - see this answerTherefore:
[CmdletBinding()]
param(
[Parameter(Mandatory, HelpMessage="password?")]
[SecureString] $password
)
# Note the use of $(...) and the enclosure of the whole argument in "..."
do-something "password=$([Runtime.InteropServices.Marshal]::PtrToStringBSTR([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password)))"
Also note:
On Windows it doesn't make a difference (and on Unix [securestring]
instances offer virtually no protection and should be avoided altogether), but it should be [Runtime.InteropServices.Marshal]::PtrToStringBSTR()
, not [Runtime.InteropServices.Marshal]::PtrToStringAuto()
As Santiago Squarzon points out, there is an easier way to convert a SecureString
instance to its plain-text equivalent (which should generally be avoided[1], however, and, more fundamentally, use of [securestring]
in new projects is discouraged[2]):
[pscredential]::new('unused', $password).GetNetworkCredential().Password
[1] A plain-text representation of a password stored in a .NET string lingers in memory for an unspecified time that you cannot control. More specifically, if it is part of a process command line, as in your case, it can be discovered that way. Of course, if the CLI you're targeting offers no way to authenticate other than with a plain-text password, you have no other choice.
[2] See this answer, which links to this .NET platform-compatibility recommendation.